From 18e48baf4701ae6f42ed0385011a4a1385997b9d Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 10 Oct 2024 15:13:20 +0300 Subject: [PATCH 01/57] SLIB-47 - update java to version 17 and update dependencies (#85) * SLIB-47 - update certs, documents and identifiers used in tests; improve tests * SLIB-47 - update java to version 17 * SLIB-47 - update slf4j-api * SLIB-47 - update jackson * SLIB-47 - add direct dependency jakarta.ws.rs-api * SLIB-47 - update jersey dependencies * SLIB-47 - update resteasy dependencies and wiremock * SLIB-47 - update identifiers used for proxy tests * SLIB-47 - update bouncy-castles artifact * SLIB-47 - update junit to version 5.11.0 and fix tests * SLIB-47 - update hamcrest and mockito * SLIB-47 - add maven-surefire-plugin to fix test report * SLIB-47 - update maven plugins * SLIB-47 - fix typos; make code and code style improvements; * SLIB-47 - replaced deprecated X509Certificate::getSubjectDN() * SLIB-47 - remove using java8 and java11 in travis configuration * SLIB-47 - update jaxb-runtime * SLIB-47 - add license header for files * SLIB-47 - update third party licenses * SLIB-47 - update README.md and version * SLIB-47 - restructure CHANGELOG description for 3.0 * SLIB-47 - modify handling of LV person codes with prefixes 33-39 * SLIB-47 - replace usage of X509Certificate::getSubjectDN() in SmartIdClientTest * SLIB-47 - restructure code in AuthenticationResponseValidator * SLIB-47 - update certificates used in tests --- .travis.yml | 2 - CHANGELOG.md | 15 + LICENSE.3RD-PARTY | 174 +- README.md | 16 +- pom.xml | 72 +- .../ee/sk/smartid/AuthenticationIdentity.java | 2 +- .../smartid/AuthenticationRequestBuilder.java | 8 +- .../AuthenticationResponseValidator.java | 463 ++-- .../java/ee/sk/smartid/CertificateLevel.java | 2 +- .../sk/smartid/CertificateRequestBuilder.java | 12 +- src/main/java/ee/sk/smartid/HashType.java | 6 +- src/main/java/ee/sk/smartid/SignableData.java | 4 +- src/main/java/ee/sk/smartid/SignableHash.java | 4 +- .../sk/smartid/SignatureRequestBuilder.java | 16 +- .../ee/sk/smartid/SmartIdRequestBuilder.java | 5 +- .../ee/sk/smartid/rest/LoggingFilter.java | 198 +- .../sk/smartid/rest/SessionStatusPoller.java | 3 +- .../sk/smartid/rest/SmartIdRestConnector.java | 10 +- .../ee/sk/smartid/rest/dao/Interaction.java | 17 +- .../sk/smartid/rest/dao/InteractionFlow.java | 2 +- .../smartid/rest/dao/RequestProperties.java | 26 + .../smartid/rest/dao/SemanticsIdentifier.java | 2 +- .../ee/sk/smartid/rest/dao/SessionStatus.java | 2 +- .../rest/dao/SessionStatusRequest.java | 6 +- .../util/CertificateAttributeUtil.java | 61 +- .../util/NationalIdentityNumberUtil.java | 68 +- .../java/ee/sk/smartid/util/StringUtil.java | 4 +- src/test/java/ee/sk/CertificateUtil.java | 56 + src/test/java/ee/sk/FileUtil.java | 55 + src/test/java/ee/sk/SmartIdDemoCondition.java | 52 + .../ee/sk/SmartIdDemoIntegrationTest.java | 40 + .../smartid/AuthenticationIdentityTest.java | 13 +- .../AuthenticationRequestBuilderTest.java | 1181 ++++----- .../AuthenticationResponseValidatorTest.java | 606 +++-- .../ee/sk/smartid/CertificateLevelTest.java | 84 +- .../ee/sk/smartid/CertificateParserTest.java | 10 +- .../CertificateRequestBuilderTest.java | 588 ++--- .../sk/smartid/ClientRequestHeaderFilter.java | 2 +- .../ee/sk/smartid/DigestCalculatorTest.java | 30 +- src/test/java/ee/sk/smartid/DummyData.java | 2 +- ...ndpointSslVerificationIntegrationTest.java | 284 +-- .../java/ee/sk/smartid/SignableDataTest.java | 72 +- .../java/ee/sk/smartid/SignableHashTest.java | 37 +- .../smartid/SignatureRequestBuilderTest.java | 1009 ++++---- .../SmartIdAuthenticationResponseTest.java | 68 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 2232 +++++++++-------- .../sk/smartid/SmartIdRestServiceStubs.java | 157 +- .../ee/sk/smartid/SmartIdSignatureTest.java | 56 +- .../VerificationCodeCalculatorTest.java | 52 +- .../smartid/rest/SessionStatusPollerTest.java | 314 +-- .../rest/SmartIdRestConnectorTest.java | 1356 +++++----- .../rest/SmartIdRestIntegrationTest.java | 390 +-- .../rest/dao/SemanticsIdentifierTest.java | 7 +- .../rest/dao/SignatureSessionRequestTest.java | 17 +- .../util/CertificateAttributeUtilTest.java | 67 +- .../util/NationalIdentityNumberUtilTest.java | 60 +- .../test/smartid/integration/ReadmeTest.java | 359 ++- .../integration/SmartIdIntegrationTest.java | 180 +- .../demo_server_trusted_ssl_certs.jks | Bin 3864 -> 5685 bytes .../demo_server_trusted_ssl_certs.p12 | Bin 4258 -> 6202 bytes src/test/resources/sid_demo_sk_ee.pem | 39 + src/test/resources/sid_live_sk_ee.pem | 38 + 62 files changed, 5615 insertions(+), 5098 deletions(-) create mode 100644 src/test/java/ee/sk/CertificateUtil.java create mode 100644 src/test/java/ee/sk/FileUtil.java create mode 100644 src/test/java/ee/sk/SmartIdDemoCondition.java create mode 100644 src/test/java/ee/sk/SmartIdDemoIntegrationTest.java create mode 100644 src/test/resources/sid_demo_sk_ee.pem create mode 100644 src/test/resources/sid_live_sk_ee.pem diff --git a/.travis.yml b/.travis.yml index 285b7238..d3aba720 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: java jdk: -- openjdk8 -- openjdk11 - openjdk17 sudo: required env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 088237e4..1eea0cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.0] - upcoming +### Changed +- Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` +- Typo fixes, code cleanup and improvements +- Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. + +### Java and dependency updates +- Updated java to version 17 +- Updated slf4j-api to version 2.0.16 +- Updated jackson dependencies to version 2.17.2 +- Added jakarta.ws.rs:jakarta.ws.rs-api +- Updated jersey dependencies to version 3.1.8 +- Updated bouncy-castle artifact to bcprov-jdk18on on version 1.78.1 +- Updated jaxb-runtime to version 4.0.5 + ## [2.3] - 2023-05-06 - To request the IP address of the device running Smart-ID app, the following methods were added: - AuthenticationRequestBuilder.withShareMdClientIpAddress(boolean) diff --git a/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index bc4ba2b1..70cbfde8 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,66 +1,110 @@ -List of 64 third-party dependencies (auto-generated on 2022-02-08 with License Maven Plugin): +List of 108 third-party dependencies (auto-generated on 2024-09-27 with License Maven Plugin): -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.2.10 - http://logback.qos.ch/logback-classic) -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.2.10 - http://logback.qos.ch/logback-core) -* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.12.3 - http://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.12.3 - https://github.com/FasterXML/jackson-core) -* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.12.3 - http://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson module: JAXB Annotations (com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.3 - https://github.com/FasterXML/jackson-modules-base) -* (The Apache Software License, Version 2.0) zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.2.1 - https://github.com/flipkart-incubator/zjsonpatch/) -* (The Apache Software License, Version 2.0) WireMock (com.github.tomakehurst:wiremock:2.4.1 - http://wiremock.org) -* (The Apache Software License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:18.0 - http://code.google.com/p/guava-libraries/guava) -* (The Apache Software License, Version 2.0) Json Path (com.jayway.jsonpath:json-path:2.2.0 - https://github.com/jayway/JsonPath) -* (EDL 1.0) Jakarta Activation (com.sun.activation:jakarta.activation:1.2.2 - https://github.com/eclipse-ee4j/jaf/jakarta.activation) -* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:3.0.11 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) -* (The Apache Software License, Version 2.0) Apache Commons Codec (commons-codec:commons-codec:1.9 - http://commons.apache.org/proper/commons-codec/) -* (The Apache Software License, Version 2.0) Apache Commons Logging (commons-logging:commons-logging:1.2 - http://commons.apache.org/proper/commons-logging/) -* (EDL 1.0) JavaBeans Activation Framework API jar (jakarta.activation:jakarta.activation-api:1.2.1 - https://github.com/eclipse-ee4j/jaf/jakarta.activation-api) -* (EPL 2.0) (GPL2 w/ CPE) jakarta.ws.rs-api (jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 - https://github.com/eclipse-ee4j/jaxrs-api) -* (Eclipse Distribution License - v 1.0) jakarta.xml.bind-api (jakarta.xml.bind:jakarta.xml.bind-api:2.3.2 - https://github.com/eclipse-ee4j/jaxb-api/jakarta.xml.bind-api) -* (CDDL + GPLv2 with classpath exception) javax.annotation API (javax.annotation:javax.annotation-api:1.2 - http://jcp.org/en/jsr/detail?id=250) -* (CDDL + GPLv2 with classpath exception) Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net) -* (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:2.0.1 - http://jax-rs-spec.java.net) -* (Eclipse Public License 1.0) JUnit (junit:junit:4.13.2 - http://junit.org) -* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.7 - https://bytebuddy.net/byte-buddy) -* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.12.7 - https://bytebuddy.net/byte-buddy-agent) -* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:1.1 - http://accessors-smart/) -* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.2.1 - http://www.minidev.net/) -* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:4.9 - http://pholser.github.com/jopt-simple) -* (The Apache Software License, Version 2.0) Apache Commons Collections (org.apache.commons:commons-collections4:4.0 - http://commons.apache.org/proper/commons-collections/) -* (Apache License, Version 2.0) Apache Commons Lang (org.apache.commons:commons-lang3:3.4 - http://commons.apache.org/proper/commons-lang/) -* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.1 - http://hc.apache.org/httpcomponents-client) -* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.3 - http://hc.apache.org/httpcomponents-core-ga) -* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.69 - https://www.bouncycastle.org/java.html) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Security (org.eclipse.jetty:jetty-security:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.2.13.v20150730 - http://www.eclipse.org/jetty) -* (CDDL + GPLv2 with classpath exception) HK2 API module (org.glassfish.hk2:hk2-api:2.5.0-b05 - https://hk2.java.net/hk2-api) -* (CDDL + GPLv2 with classpath exception) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:2.5.0-b05 - https://hk2.java.net/hk2-locator) -* (CDDL + GPLv2 with classpath exception) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:2.5.0-b05 - https://hk2.java.net/hk2-utils) -* (CDDL + GPLv2 with classpath exception) OSGi resource locator bundle - used by various API providers that rely on META-INF/services mechanism to locate providers. (org.glassfish.hk2:osgi-resource-locator:1.0.1 - http://glassfish.org/osgi-resource-locator/) -* (CDDL + GPLv2 with classpath exception) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:2.5.0-b05 - https://hk2.java.net/external/aopalliance-repackaged) -* (CDDL + GPLv2 with classpath exception) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:2.5.0-b05 - https://hk2.java.net/external/javax.inject) -* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:2.3.3 - https://eclipse-ee4j.github.io/jaxb-ri/jaxb-runtime-parent/jaxb-runtime) -* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:2.3.3 - https://eclipse-ee4j.github.io/jaxb-ri/jaxb-txw-parent/txw2) -* (CDDL+GPL License) jersey-repackaged-guava (org.glassfish.jersey.bundles.repackaged:jersey-guava:2.24.1 - https://jersey.java.net/project/project/jersey-guava/) -* (CDDL+GPL License) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:2.24.1 - https://jersey.java.net/project/jersey-apache-connector/) -* (CDDL+GPL License) jersey-core-client (org.glassfish.jersey.core:jersey-client:2.24.1 - https://jersey.java.net/jersey-client/) -* (CDDL+GPL License) jersey-core-common (org.glassfish.jersey.core:jersey-common:2.24.1 - https://jersey.java.net/jersey-common/) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:2.32 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) -* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:2.32 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) -* (New BSD License) Hamcrest Core (org.hamcrest:hamcrest-core:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-core) -* (New BSD License) Hamcrest library (org.hamcrest:hamcrest-library:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-library) -* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.20.0-GA - http://www.javassist.org/) -* (The MIT License) mockito-core (org.mockito:mockito-core:4.3.1 - https://github.com/mockito/mockito) -* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) -* (BSD) ASM Core (org.ow2.asm:asm:5.0.3 - http://asm.objectweb.org/asm/) -* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:1.7.30 - http://www.slf4j.org) -* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.1.1 - http://www.xmlunit.org/) -* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.1.1 - http://www.xmlunit.org/) +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) +* (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) +* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) +* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.17.1 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1 - https://github.com/FasterXML/jackson-dataformats-text) +* (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) +* (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) +* (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.3.1 - https://github.com/jknack/handlebars.java/handlebars) +* (The Apache Software License, Version 2.0) Handlebars Helpers (com.github.jknack:handlebars-helpers:4.3.1 - https://github.com/jknack/handlebars.java/handlebars-helpers) +* (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.26.1 - https://errorprone.info/error_prone_annotations) +* (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess) +* (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) +* (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) +* (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) +* (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) +* (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) +* (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) +* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) +* (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.16.1 - https://commons.apache.org/proper/commons-codec/) +* (Apache-2.0) Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) +* (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.11.0 - https://commons.apache.org/proper/commons-io/) +* (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.1 - https://commons.apache.org/proper/commons-logging/) +* (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) +* (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) +* (The Apache Software License, Version 2.0) Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) +* (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) +* (EPL-2.0) (GPL-2.0-with-classpath-exception) Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:4.0.0 - https://github.com/jakartaee/rest/jakarta.ws.rs-api) +* (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) +* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.15.0 - https://bytebuddy.net/byte-buddy) +* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.15.0 - https://bytebuddy.net/byte-buddy-agent) +* (The Apache Software License, Version 2.0) json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.40.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) +* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) +* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) +* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) +* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) +* (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) +* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) +* (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/) +* (EDL 1.0) Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:11.0.20 - https://eclipse.dev/jetty/jetty-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:11.0.20 - https://eclipse.dev/jetty/jetty-http) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:11.0.20 - https://eclipse.dev/jetty/jetty-io) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:11.0.20 - https://eclipse.dev/jetty/jetty-proxy) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Security (org.eclipse.jetty:jetty-security:11.0.20 - https://eclipse.dev/jetty/jetty-security) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:11.0.20 - https://eclipse.dev/jetty/jetty-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:11.0.20 - https://eclipse.dev/jetty/jetty-servlet) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:11.0.20 - https://eclipse.dev/jetty/jetty-servlets) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:11.0.20 - https://eclipse.dev/jetty/jetty-util) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:11.0.20 - https://eclipse.dev/jetty/jetty-webapp) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:11.0.20 - https://eclipse.dev/jetty/jetty-xml) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-common) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-hpack) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api) +* (EPL 2.0) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) +* (EPL 2.0) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) +* (EPL 2.0) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) +* (EPL 2.0) (GPL2 w/ CPE) OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) +* (EPL 2.0) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) +* (Eclipse Distribution License - v 1.0) JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-apache-connector) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) +* (Apache License, 2.0) (EPL 2.0) (Public Domain) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) +* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) +* (BSD-3-Clause) Hamcrest (org.hamcrest:hamcrest:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Core (org.hamcrest:hamcrest-core:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Library (org.hamcrest:hamcrest-library:3.0 - http://hamcrest.org/JavaHamcrest/) +* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) +* (Apache License, Version 2.0) Java Annotation Indexer (org.jboss:jandex:2.4.5.Final - http://www.jboss.org/jandex) +* (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.3.Final - http://www.jboss.org) +* (Apache License 2.0) RESTEasy Client (org.jboss.resteasy:resteasy-client:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Client API (org.jboss.resteasy:resteasy-client-api:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core (org.jboss.resteasy:resteasy-core:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core SPI (org.jboss.resteasy:resteasy-core-spi:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Jackson 2 Provider (org.jboss.resteasy:resteasy-jackson2-provider:6.2.10.Final - https://resteasy.dev) +* (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.0 - https://junit.org/junit5/) +* (MIT) mockito-core (org.mockito:mockito-core:5.13.0 - https://github.com/mockito/mockito) +* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis) +* (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) +* (MIT-0) reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) +* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:2.0.16 - http://www.slf4j.org) +* (The Apache Software License, Version 2.0) WireMock (org.wiremock:wiremock:3.9.1 - http://wiremock.org) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) +* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.10.0 - https://www.xmlunit.org/) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.10.0 - https://www.xmlunit.org/xmlunit-placeholders/) +* (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) diff --git a/README.md b/README.md index e146e312..f3062e85 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The Smart-ID Java client can be used for easy integration of the [Smart-ID](http * creating digital signature ## Requirements -* Java 8 or later + * Java 17 or later ## Getting the library @@ -151,7 +151,7 @@ client.setTrustStore(trustStore); ### Feeding trusted certificates one by one -It also possible to feed trusted certificates one by one. +It is also possible to feed trusted certificates one by one. This can prove useful when trusted certificates are kept as application configuration property. ```java @@ -210,7 +210,7 @@ String deviceIpAddress = authenticationResponse.getDeviceIpAddress(); Note that verificationCode should be displayed by the web service, so the person signing through the Smart-ID mobile app can verify if the verification code displayed on the phone matches with the one shown on the web page. Leave a few seconds for the verification code to be displayed for users using the web service with their mobile device. -Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed. +Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed). ### Authenticating with document number @@ -280,7 +280,7 @@ from a separate field of the certificate but for some older Smart-id accounts More info about the availability of the separate field in certificates: https://github.com/SK-EID/smart-id-documentation/wiki/FAQ#where-can-i-find-users-date-of-birth -``` +```java Optional dateOfBirth = authIdentity.getDateOfBirth(); ``` @@ -289,7 +289,7 @@ and then construct authentication identity from that and extract the date-of-birth from there. Read below about how to obtain the signer's certificate. -``` +```java AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(signersCertificate); Optional dateOfBirthExtracted = identity.getDateOfBirth(); ``` @@ -313,7 +313,7 @@ X509Certificate signersCertificate = responseWithSigningCertificate.getCertifica ``` If needed you can use semantics identifier instead of document number to obtain signer's certificate. -This may trigger a notification to all of the user's devices if user has more than one device with Smart-ID +This may trigger a notification to all the user's devices if user has more than one device with Smart-ID (as each device has separate signing certificate). ### Create the signature @@ -364,7 +364,7 @@ Different interaction flows can support different amount of data to display info Available interactions: * `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. * `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. -* `confirmationMessage` with `displayText200`. First screen is for text only (max 200 chars) and has Confirm and Cancel buttons. Second screen is for PIN. +* `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. * `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. @@ -528,7 +528,7 @@ This way it is possible to reduce error handling code to only handle generic par * Enduring - Exceptions that indicate problems with incorrect integration. Usually these types of errors remain when user retries shortly. * ServerMaintenanceException - Server is currently under maintenance - * SmartIdClientException - this exception is a sign of incorrect integration with Smart-ID service (i.e. missing parameters etc) + * SmartIdClientException - this exception is a sign of incorrect integration with Smart-ID service (i.e. missing parameters etc.) * RelyingPartyAccountConfigurationException - indicates that RelyingParty configuration at Smart-ID side can be incorrect * UnprocessableSmartIdResponseException - shouldn't happen under normal conditions * SessionNotFoundException - When session was not found. Usually this is also caused by problems with implementation. diff --git a/pom.xml b/pom.xml index 180ff8eb..7fbba6ba 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ee.sk.smartid smart-id-java-client jar - 2.0-SNAPSHOT + 3.0-SNAPSHOT Smart-ID Java client Smart-ID Java client is a Java library that can be used for easy integration of the Smart-ID solution to information systems or e-services @@ -43,12 +43,12 @@ UTF-8 - 1.8 - 1.8 - 2.14.2 - 2.14.2 - 3.0.10 - 6.0.3.Final + 17 + 17 + 2.17.2 + 2.17.2 + 3.1.8 + 6.2.10.Final @@ -70,6 +70,12 @@ test + + org.glassfish.jaxb + jaxb-runtime + 4.0.5 + + com.fasterxml.jackson.core jackson-annotations @@ -84,52 +90,55 @@ org.slf4j slf4j-api - 1.7.36 + 2.0.16 - - org.glassfish.jaxb - jaxb-runtime + jakarta.ws.rs + jakarta.ws.rs-api 4.0.0 - org.bouncycastle - bcprov-jdk15on - 1.70 + bcprov-jdk18on + 1.78.1 - - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter-api + 5.11.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.11.0 test org.hamcrest hamcrest-library - 1.3 + 3.0 test ch.qos.logback logback-classic - 1.2.11 + 1.5.8 test - com.github.tomakehurst + org.wiremock wiremock - 2.27.2 + 3.9.1 test org.mockito mockito-core - 4.7.0 + 5.13.0 test @@ -151,10 +160,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.12 @@ -174,7 +188,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.3.1 attach-sources @@ -188,7 +202,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.1 + 3.10.0 attach-javadocs @@ -202,7 +216,7 @@ org.codehaus.mojo license-maven-plugin - 1.16 + 2.4.0 create-license-list @@ -250,13 +264,13 @@ com.github.spotbugs spotbugs-maven-plugin - 3.1.12 + 4.8.6.4 org.apache.maven.plugins maven-jar-plugin - 3.2.2 + 3.4.2 diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index 420b053a..5a8af3fd 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -73,7 +73,7 @@ public String getSurName() { /** * @param surName surname - * + *

* Instead use: * {@link #setSurname(String)} */ diff --git a/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java index 42b85de2..dd6c8456 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java @@ -189,7 +189,7 @@ public AuthenticationRequestBuilder withCertificateLevel(String certificateLevel /** * Sets the request's nonce *

- * By default the authentication's initiation request + * By default, the authentication's initiation request * has idempotent behaviour meaning when the request * is repeated inside a given time frame with exactly * the same parameters, session ID of an existing session @@ -210,7 +210,7 @@ public AuthenticationRequestBuilder withNonce(String nonce) { /** * Specifies capabilities of the user *

- * By default there are no specified capabilities. + * By default, there are no specified capabilities. * The capabilities need to be specified in case of * a restricted Smart ID user * {@link #withCapabilities(String...)} @@ -285,12 +285,12 @@ public SmartIdAuthenticationResponse authenticate() throws UserAccountNotFoundEx } /** - * Send the authentication request and get the session Id + * Send the authentication request and get the session ID * * @throws UserAccountNotFoundException when the user account was not found * @throws ServerMaintenanceException when the server is under maintenance * - * @return session Id - later to be used for manual session status polling + * @return session ID - later to be used for manual session status polling */ public String initiateAuthentication() throws UserAccountNotFoundException, ServerMaintenanceException { validateParameters(); diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index e295e8ea..3837dabf 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,283 +26,272 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.NationalIdentityNumberUtil; -import ee.sk.smartid.util.StringUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static java.util.Arrays.asList; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.security.*; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.LocalDate; -import java.util.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; -import static java.util.Arrays.asList; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.NationalIdentityNumberUtil; +import ee.sk.smartid.util.StringUtil; /** * Class used to validate the authentication */ public class AuthenticationResponseValidator { - private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseValidator.class); - - private List trustedCACertificates = new ArrayList<>(); - /** - * Constructs a new {@code AuthenticationResponseValidator}. - *

- * The constructed instance is initialized with default trusted - * CA certificates. - * - * @throws SmartIdClientException when there was an error initializing trusted CA certificates - */ - public AuthenticationResponseValidator() { - initializeTrustedCACertificatesFromKeyStore(); - } + private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseValidator.class); - /** - * Constructs a new {@code AuthenticationResponseValidator}. - *

- * The constructed instance is initialized passed in certificates. - * @param trustedCertificates List of certificates to trust - * - * @throws SmartIdClientException when there was an error initializing trusted CA certificates - */ - public AuthenticationResponseValidator(X509Certificate[] trustedCertificates) { - trustedCACertificates.addAll(asList(trustedCertificates)); - } + private final List trustedCACertificates = new ArrayList<>(); - /** - * Validates the authentication response and returns the result. - * Performs the following validations: - * "result.endResult" has the value "OK" - * "signature.value" is the valid signature over the same "hash", which was submitted by the RP. - * "signature.value" is the valid signature, verifiable with the public key inside the certificate of the user, given in the field "cert.value" - * The person's certificate given in the "cert.value" is valid (not expired, signed by trusted CA and with correct (i.e. the same as in response structure, greater than or equal to that in the original request) level). - * - * @param authenticationResponse authentication response to be validated - * @return authentication result - */ - public AuthenticationIdentity validate(SmartIdAuthenticationResponse authenticationResponse) { - validateAuthenticationResponse(authenticationResponse); - AuthenticationIdentity identity = constructAuthenticationIdentity(authenticationResponse.getCertificate()); - if (!verifyResponseEndResult(authenticationResponse)) { - throw new UnprocessableSmartIdResponseException("Smart-ID API returned end result code '" + authenticationResponse.getEndResult() + "'"); - } - if (!verifySignature(authenticationResponse)) { - throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); + /** + * Constructs a new {@code AuthenticationResponseValidator}. + *

+ * The constructed instance is initialized with default trusted + * CA certificates. + * + * @throws SmartIdClientException when there was an error initializing trusted CA certificates + */ + public AuthenticationResponseValidator() { + initializeTrustedCACertificatesFromKeyStore(); } - if (!verifyCertificateExpiry(authenticationResponse.getCertificate())) { - throw new UnprocessableSmartIdResponseException("Signer's certificate has expired"); - } - if (!isCertificateTrusted(authenticationResponse.getCertificate())) { - throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); - } - if (!verifyCertificateLevel(authenticationResponse)) { - throw new CertificateLevelMismatchException(); - } - return identity; - } - - /** - * Gets the list of trusted CA certificates - *

- * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @return list of trusted CA certificates - */ - public List getTrustedCACertificates() { - return trustedCACertificates; - } - - /** - * Adds a certificate to the list of trusted CA certificates - *

- * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @param certificate trusted CA certificate - */ - public void addTrustedCACertificate(X509Certificate certificate) { - trustedCACertificates.add(certificate); - } - - /** - * Constructs a certificate from the byte array and - * adds it into the list of trusted CA certificates - *

- * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @throws CertificateException when there was an error constructing the certificate from bytes - * - * @param certificateBytes trusted CA certificate - */ - public void addTrustedCACertificate(byte[] certificateBytes) throws CertificateException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); - addTrustedCACertificate(caCertificate); - } - /** - * Constructs a certificate from the file - * and adds it into the list of trusted CA certificates - *

- * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @throws IOException when there is an error reading the file - * @throws CertificateException when there is an error constructing the certificate from the bytes of the file - * - * @param certificateFile trusted CA certificate - */ - public void addTrustedCACertificate(File certificateFile) throws IOException, CertificateException { - addTrustedCACertificate(Files.readAllBytes(certificateFile.toPath())); - } - - /** - * Clears the list of trusted CA certificates - *

- * PS! When clearing the trusted CA certificates - * make sure it is not left empty. In that case - * there is impossible to verify the trust of the - * authenticating person. - */ - public void clearTrustedCACertificates() { - trustedCACertificates.clear(); - } - - private void initializeTrustedCACertificatesFromKeyStore() { - try (InputStream is = AuthenticationResponseValidator.class.getResourceAsStream("/trusted_certificates.jks")) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, "changeit".toCharArray()); - Enumeration aliases = keystore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - addTrustedCACertificate(certificate); - } - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing trusted CA certificates", e); - throw new SmartIdClientException("Error initializing trusted CA certificates", e); + /** + * Constructs a new {@code AuthenticationResponseValidator}. + *

+ * The constructed instance is initialized passed in certificates. + * + * @param trustedCertificates List of certificates to trust + * @throws SmartIdClientException when there was an error initializing trusted CA certificates + */ + public AuthenticationResponseValidator(X509Certificate[] trustedCertificates) { + trustedCACertificates.addAll(asList(trustedCertificates)); } - } - private void validateAuthenticationResponse(SmartIdAuthenticationResponse authenticationResponse) { - if (authenticationResponse.getCertificate() == null) { - logger.error("Certificate is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Certificate is not present in the authentication response"); - } - if (StringUtil.isEmpty(authenticationResponse.getSignatureValueInBase64())) { - logger.error("Signature is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Signature is not present in the authentication response"); - } - if (authenticationResponse.getHashType() == null) { - logger.error("Hash type is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Hash type is not present in the authentication response"); + /** + * Validates the authentication response and returns the result. + * Performs the following validations: + * "result.endResult" has the value "OK" + * "signature.value" is the valid signature over the same "hash", which was submitted by the RP. + * "signature.value" is the valid signature, verifiable with the public key inside the certificate of the user, given in the field "cert.value" + * The person's certificate given in the "cert.value" is valid (not expired, signed by trusted CA and with correct (i.e. the same as in response structure, greater than or equal to that in the original request) level). + * + * @param authenticationResponse authentication response to be validated + * @return authentication result + */ + public AuthenticationIdentity validate(SmartIdAuthenticationResponse authenticationResponse) { + validateAuthenticationResponse(authenticationResponse); + AuthenticationIdentity identity = constructAuthenticationIdentity(authenticationResponse.getCertificate()); + if (!verifyResponseEndResult(authenticationResponse)) { + throw new UnprocessableSmartIdResponseException("Smart-ID API returned end result code '" + authenticationResponse.getEndResult() + "'"); + } + if (!verifySignature(authenticationResponse)) { + throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); + } + if (!verifyCertificateExpiry(authenticationResponse.getCertificate())) { + throw new UnprocessableSmartIdResponseException("Signer's certificate has expired"); + } + if (!isCertificateTrusted(authenticationResponse.getCertificate())) { + throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); + } + if (!verifyCertificateLevel(authenticationResponse)) { + throw new CertificateLevelMismatchException(); + } + return identity; } - } - private boolean verifyResponseEndResult(SmartIdAuthenticationResponse authenticationResponse) { - return "OK".equalsIgnoreCase(authenticationResponse.getEndResult()); - } + /** + * Gets the list of trusted CA certificates + *

+ * Authenticating person's certificate has to be issued by + * one of the trusted CA certificates. Otherwise, the person's + * authentication is deemed untrusted and therefore not valid. + * + * @return list of trusted CA certificates + */ + public List getTrustedCACertificates() { + return trustedCACertificates; + } - private boolean verifySignature(SmartIdAuthenticationResponse authenticationResponse) { - try { - PublicKey signersPublicKey = authenticationResponse.getCertificate().getPublicKey(); - Signature signature = Signature.getInstance("NONEwith" + signersPublicKey.getAlgorithm()); - signature.initVerify(signersPublicKey); - byte[] signedHash = Base64.getDecoder().decode(authenticationResponse.getSignedHashInBase64()); - byte[] signedDigestWithPadding = addPadding(authenticationResponse.getHashType().getDigestInfoPrefix(), signedHash); - signature.update(signedDigestWithPadding); - return signature.verify(authenticationResponse.getSignatureValue()); - } catch (GeneralSecurityException e) { - logger.error("Signature verification failed"); - throw new UnprocessableSmartIdResponseException("Signature verification failed", e); + /** + * Adds a certificate to the list of trusted CA certificates + *

+ * Authenticating person's certificate has to be issued by + * one of the trusted CA certificates. Otherwise, the person's + * authentication is deemed untrusted and therefore not valid. + * + * @param certificate trusted CA certificate + */ + public void addTrustedCACertificate(X509Certificate certificate) { + trustedCACertificates.add(certificate); } - } - private boolean verifyCertificateExpiry(X509Certificate certificate) { - return !certificate.getNotAfter().before(new Date()); - } + /** + * Constructs a certificate from the byte array and + * adds it into the list of trusted CA certificates + *

+ * Authenticating person's certificate has to be issued by + * one of the trusted CA certificates. Otherwise, the person's + * authentication is deemed untrusted and therefore not valid. + * + * @param certificateBytes trusted CA certificate + * @throws CertificateException when there was an error constructing the certificate from bytes + */ + public void addTrustedCACertificate(byte[] certificateBytes) throws CertificateException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + addTrustedCACertificate(caCertificate); + } - private boolean isCertificateTrusted(X509Certificate certificate) { - for (X509Certificate trustedCACertificate : trustedCACertificates) { - try { - certificate.verify(trustedCACertificate.getPublicKey()); - logger.info("Certificate verification passed for '{}' against CA certificate '{}' ", certificate.getSubjectDN() ,trustedCACertificate.getSubjectDN() ); + /** + * Constructs a certificate from the file + * and adds it into the list of trusted CA certificates + *

+ * Authenticating person's certificate has to be issued by + * one of the trusted CA certificates. Otherwise, the person's + * authentication is deemed untrusted and therefore not valid. + * + * @param certificateFile trusted CA certificate + * @throws IOException when there is an error reading the file + * @throws CertificateException when there is an error constructing the certificate from the bytes of the file + */ + public void addTrustedCACertificate(File certificateFile) throws IOException, CertificateException { + addTrustedCACertificate(Files.readAllBytes(certificateFile.toPath())); + } - return true; - } catch (GeneralSecurityException e) { - logger.debug("Error verifying signer's certificate: " + certificate.getSubjectDN() + " against CA certificate: " + trustedCACertificate.getSubjectDN(), e); - } + /** + * Clears the list of trusted CA certificates + *

+ * PS! When clearing the trusted CA certificates + * make sure it is not left empty. In that case + * there is impossible to verify the trust of the + * authenticating person. + */ + public void clearTrustedCACertificates() { + trustedCACertificates.clear(); } - return false; - } - private boolean verifyCertificateLevel(SmartIdAuthenticationResponse authenticationResponse) { - CertificateLevel certLevel = new CertificateLevel(authenticationResponse.getCertificateLevel()); - String requestedCertificateLevel = authenticationResponse.getRequestedCertificateLevel(); - return StringUtil.isEmpty(requestedCertificateLevel) || certLevel.isEqualOrAbove(requestedCertificateLevel); - } + public static AuthenticationIdentity constructAuthenticationIdentity(X509Certificate certificate) { + AuthenticationIdentity identity = new AuthenticationIdentity(certificate); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) + .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); + identity.setDateOfBirth(getDateOfBirth(identity)); + return identity; + } - private static byte[] addPadding(byte[] digestInfoPrefix, byte[] digest) { - final byte[] digestWithPrefix = new byte[digestInfoPrefix.length + digest.length]; - System.arraycopy(digestInfoPrefix, 0, digestWithPrefix, 0, digestInfoPrefix.length); - System.arraycopy(digest, 0, digestWithPrefix, digestInfoPrefix.length, digest.length); - return digestWithPrefix; - } + private void initializeTrustedCACertificatesFromKeyStore() { + try (InputStream is = AuthenticationResponseValidator.class.getResourceAsStream("/trusted_certificates.jks")) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, "changeit".toCharArray()); + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + addTrustedCACertificate(certificate); + } + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing trusted CA certificates", e); + throw new SmartIdClientException("Error initializing trusted CA certificates", e); + } + } - public static AuthenticationIdentity constructAuthenticationIdentity(X509Certificate certificate) { - AuthenticationIdentity identity = new AuthenticationIdentity(certificate); - try { - LdapName ln = new LdapName(certificate.getSubjectDN().getName()); - for(Rdn rdn : ln.getRdns()) { - if (rdn.getType().equalsIgnoreCase("GIVENNAME")) { - identity.setGivenName(rdn.getValue().toString()); + private void validateAuthenticationResponse(SmartIdAuthenticationResponse authenticationResponse) { + if (authenticationResponse.getCertificate() == null) { + logger.error("Certificate is not present in the authentication response"); + throw new UnprocessableSmartIdResponseException("Certificate is not present in the authentication response"); } - else if (rdn.getType().equalsIgnoreCase("SURNAME")) { - identity.setSurname(rdn.getValue().toString()); + if (StringUtil.isEmpty(authenticationResponse.getSignatureValueInBase64())) { + logger.error("Signature is not present in the authentication response"); + throw new UnprocessableSmartIdResponseException("Signature is not present in the authentication response"); } - else if (rdn.getType().equalsIgnoreCase("SERIALNUMBER")) { - identity.setIdentityNumber(rdn.getValue().toString().split("-", 2)[1]); + if (authenticationResponse.getHashType() == null) { + logger.error("Hash type is not present in the authentication response"); + throw new UnprocessableSmartIdResponseException("Hash type is not present in the authentication response"); } - else if (rdn.getType().equalsIgnoreCase("C")) { - identity.setCountry(rdn.getValue().toString()); + } + + private boolean verifyResponseEndResult(SmartIdAuthenticationResponse authenticationResponse) { + return "OK".equalsIgnoreCase(authenticationResponse.getEndResult()); + } + + private boolean verifySignature(SmartIdAuthenticationResponse authenticationResponse) { + try { + PublicKey signersPublicKey = authenticationResponse.getCertificate().getPublicKey(); + Signature signature = Signature.getInstance("NONEwith" + signersPublicKey.getAlgorithm()); + signature.initVerify(signersPublicKey); + byte[] signedHash = Base64.getDecoder().decode(authenticationResponse.getSignedHashInBase64()); + byte[] signedDigestWithPadding = addPadding(authenticationResponse.getHashType().getDigestInfoPrefix(), signedHash); + signature.update(signedDigestWithPadding); + return signature.verify(authenticationResponse.getSignatureValue()); + } catch (GeneralSecurityException e) { + logger.error("Signature verification failed"); + throw new UnprocessableSmartIdResponseException("Signature verification failed", e); } - } + } - identity.setDateOfBirth(getDateOfBirth(identity)); + private boolean verifyCertificateExpiry(X509Certificate certificate) { + return !certificate.getNotAfter().before(new Date()); + } + + private boolean isCertificateTrusted(X509Certificate certificate) { + for (X509Certificate trustedCACertificate : trustedCACertificates) { + try { + certificate.verify(trustedCACertificate.getPublicKey()); + logger.info("Certificate verification passed for '{}' against CA certificate '{}' ", certificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal()); - return identity; + return true; + } catch (GeneralSecurityException e) { + logger.debug("Error verifying signer's certificate: {} against CA certificate: {}", certificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal(), e); + } + } + return false; } - catch (InvalidNameException e) { - logger.error("Error getting authentication identity from the certificate", e); - throw new SmartIdClientException("Error getting authentication identity from the certificate", e); + + private boolean verifyCertificateLevel(SmartIdAuthenticationResponse authenticationResponse) { + CertificateLevel certLevel = new CertificateLevel(authenticationResponse.getCertificateLevel()); + String requestedCertificateLevel = authenticationResponse.getRequestedCertificateLevel(); + return StringUtil.isEmpty(requestedCertificateLevel) || certLevel.isEqualOrAbove(requestedCertificateLevel); } - } - public static LocalDate getDateOfBirth(AuthenticationIdentity identity) { - return Optional.ofNullable( - CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) - .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); - } + private static byte[] addPadding(byte[] digestInfoPrefix, byte[] digest) { + final byte[] digestWithPrefix = new byte[digestInfoPrefix.length + digest.length]; + System.arraycopy(digestInfoPrefix, 0, digestWithPrefix, 0, digestInfoPrefix.length); + System.arraycopy(digest, 0, digestWithPrefix, digestInfoPrefix.length, digest.length); + return digestWithPrefix; + } + private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { + return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) + .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); + } } diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index b72be40f..31203b7b 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -32,7 +32,7 @@ public class CertificateLevel { - private String certificateLevel; + private final String certificateLevel; private static final Map certificateLevels = new HashMap<>(); diff --git a/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java index 4525ec36..d37294d3 100644 --- a/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java @@ -144,7 +144,7 @@ public CertificateRequestBuilder withCertificateLevel(String certificateLevel) { /** * Sets the request's nonce *

- * By default the certificate choice's initiation request + * By default, the certificate choice's initiation request * has idempotent behaviour meaning when the request * is repeated inside a given time frame with exactly * the same parameters, session ID of an existing session @@ -165,7 +165,7 @@ public CertificateRequestBuilder withNonce(String nonce) { /** * Specifies capabilities of the user *

- * By default there are no specified capabilities. + * By default, there are no specified capabilities. * The capabilities need to be specified in case of * a restricted Smart ID user * {@link #withCapabilities(String...)} @@ -182,7 +182,7 @@ public CertificateRequestBuilder withCapabilities(Capability... capabilities) { * Specifies capabilities of the user *

* - * By default there are no specified capabilities. + * By default, there are no specified capabilities. * The capabilities need to be specified in case of * a restricted Smart ID user * {@link #withCapabilities(Capability...)} @@ -254,12 +254,12 @@ public SmartIdCertificate fetch() throws UserAccountNotFoundException, UserRefus } /** - * Send the certificate choice request and get the session Id + * Send the certificate choice request and get the session ID * * @throws UserAccountNotFoundException when the user account was not found * @throws ServerMaintenanceException when the server is under maintenance * - * @return session Id - later to be used for manual session status polling + * @return session ID - later to be used for manual session status polling */ public String initiateCertificateChoice() throws UserAccountNotFoundException, SmartIdClientException, ServerMaintenanceException { @@ -273,7 +273,7 @@ public String initiateCertificateChoice() throws UserAccountNotFoundException, * Create {@link SmartIdCertificate} from {@link SessionStatus} *

* This method uses automatic session status polling internally - * and therefore blocks the current thread until certificate choice is concluded/interupted etc. + * and therefore blocks the current thread until certificate choice is concluded/interrupted etc. * * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe diff --git a/src/main/java/ee/sk/smartid/HashType.java b/src/main/java/ee/sk/smartid/HashType.java index 89d02a3e..426c7f44 100644 --- a/src/main/java/ee/sk/smartid/HashType.java +++ b/src/main/java/ee/sk/smartid/HashType.java @@ -32,9 +32,9 @@ public enum HashType { SHA384("SHA-384", "SHA384", new byte[] { 0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30 }), SHA512("SHA-512", "SHA512", new byte[] { 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40 }); - private String algorithmName; - private String hashTypeName; - private byte[] digestInfoPrefix; + private final String algorithmName; + private final String hashTypeName; + private final byte[] digestInfoPrefix; HashType(String algorithmName, String hashTypeName, byte[] digestInfoPrefix) { this.algorithmName = algorithmName; diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java index ae54b887..b085ad0d 100644 --- a/src/main/java/ee/sk/smartid/SignableData.java +++ b/src/main/java/ee/sk/smartid/SignableData.java @@ -34,7 +34,7 @@ * to be signed when it is not yet in hashed format *

* {@link #setHashType(HashType)} can be used - * to set the wanted hash tpye. SHA-512 is default. + * to set the wanted hash type. SHA-512 is default. *

* {@link #calculateHash()} and * {@link #calculateHashInBase64()} methods @@ -46,7 +46,7 @@ */ public class SignableData implements Serializable { - private byte[] dataToSign; + private final byte[] dataToSign; private HashType hashType = HashType.SHA512; public SignableData(byte[] dataToSign) { diff --git a/src/main/java/ee/sk/smartid/SignableHash.java b/src/main/java/ee/sk/smartid/SignableHash.java index 902bdc09..e3e92140 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/SignableHash.java @@ -36,7 +36,7 @@ * {@link #setHash(byte[])} can be used * to set the hash. * {@link #setHashType(HashType)} can be used - * to set the hash tpye. + * to set the hash type. *

* {@link ee.sk.smartid.SignableData} can be used * instead when the data to be signed is not already @@ -75,7 +75,7 @@ public boolean areFieldsFilled() { * Calculates the verification code from the hash *

* Verification code should be displayed on the web page or some sort of web service - * so the person signing through the Smart-ID mobile app can verify if if the verification code + * so the person signing through the Smart-ID mobile app can verify if the verification code * displayed on the phone matches with the one shown on the web page. * * @return the verification code diff --git a/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java index ad358c27..043b2ccf 100644 --- a/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java @@ -163,7 +163,7 @@ public SignatureRequestBuilder withSemanticsIdentifier(SemanticsIdentifier seman * can be used to select the wanted hash type * and the data is hashed for you. * - * @param dataToSign dat to be signed + * @param dataToSign data to be signed * @return this builder */ public SignatureRequestBuilder withSignableData(SignableData dataToSign) { @@ -203,7 +203,7 @@ public SignatureRequestBuilder withCertificateLevel(String certificateLevel) { /** * Sets the request's nonce *

- * By default the signature's initiation request + * By default, the signature's initiation request * has idempotent behaviour meaning when the request * is repeated inside a given time frame with exactly * the same parameters, session ID of an existing session @@ -224,7 +224,7 @@ public SignatureRequestBuilder withNonce(String nonce) { /** * Specifies capabilities of the user *

- * By default there are no specified capabilities. + * By default, there are no specified capabilities. * The capabilities need to be specified in case of * a restricted Smart ID user * {@link #withCapabilities(String...)} @@ -241,7 +241,7 @@ public SignatureRequestBuilder withCapabilities(Capability... capabilities) { * Specifies capabilities of the user *

* - * By default there are no specified capabilities. + * By default, there are no specified capabilities. * The capabilities need to be specified in case of * a restricted Smart ID user * {@link #withCapabilities(Capability...)} @@ -279,7 +279,7 @@ public SignatureRequestBuilder withAllowedInteractionsOrder(List al * Send the signature request and get the response *

* This method uses automatic session status polling internally - * and therefore blocks the current thread until signing is concluded/interupted etc. + * and therefore blocks the current thread until signing is concluded/interrupted etc. * * @throws UserAccountNotFoundException when the user account was not found * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. @@ -300,12 +300,12 @@ public SmartIdSignature sign() throws UserAccountNotFoundException, UserRefusedE } /** - * Send the signature request and get the session Id + * Send the signature request and get the session ID * * @throws UserAccountNotFoundException when the user account was not found * @throws ServerMaintenanceException when the server is under maintenance * - * @return session Id - later to be used for manual session status polling + * @return session ID - later to be used for manual session status polling */ public String initiateSigning() throws UserAccountNotFoundException, ServerMaintenanceException { validateParameters(); @@ -329,7 +329,7 @@ private SignatureSessionResponse getSignatureResponse(SignatureSessionRequest re * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * @throws UnprocessableSmartIdResponseException when session status response's result is missing or it has some unknown value + * @throws UnprocessableSmartIdResponseException when session status response's result is missing, or it has some unknown value * * @param sessionStatus session status response * @return the authentication response diff --git a/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java index d2cb4acf..e1524d1d 100644 --- a/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java @@ -47,8 +47,9 @@ public abstract class SmartIdRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(SmartIdRequestBuilder.class); - private SmartIdConnector connector; - private SessionStatusPoller sessionStatusPoller; + + private final SmartIdConnector connector; + private final SessionStatusPoller sessionStatusPoller; protected String relyingPartyUUID; protected String relyingPartyName; protected SemanticsIdentifier semanticsIdentifier; diff --git a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java index 1e9b69a7..a19b587c 100644 --- a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java +++ b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,6 +26,19 @@ * #L% */ +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; + +import org.glassfish.jersey.message.MessageUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; @@ -34,119 +47,112 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.WriterInterceptor; import jakarta.ws.rs.ext.WriterInterceptorContext; -import org.glassfish.jersey.message.MessageUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.net.URI; -import java.nio.charset.Charset; public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor { - private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); - private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; + + @Override + public void filter(ClientRequestContext requestContext) { + if (logger.isDebugEnabled()) { + logUrl(requestContext); + } + if (logger.isTraceEnabled()) { + logHeaders(requestContext); + if (requestContext.hasEntity()) { + wrapEntityStreamWithLogger(requestContext); + } + } + } - @Override - public void filter(ClientRequestContext requestContext) { - if (logger.isDebugEnabled()) { - logUrl(requestContext); + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); + } + if (logger.isTraceEnabled() && responseContext.hasEntity()) { + logResponseBody(responseContext); + } } - if (logger.isTraceEnabled()) { - logHeaders(requestContext); - if (requestContext.hasEntity()) { - wrapEntityStreamWithLogger(requestContext); - } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + context.proceed(); + if (logger.isTraceEnabled()) { + logRequestBody(context); + } } - } - @Override - public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { - if (logger.isDebugEnabled()) { - logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); + private void logUrl(ClientRequestContext requestContext) { + String method = requestContext.getMethod(); + URI uri = requestContext.getUri(); + logger.debug(method + " " + uri.toString()); } - if (logger.isTraceEnabled() && responseContext.hasEntity()) { - logResponseBody(responseContext); + + private void logHeaders(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getStringHeaders(); + if (headers != null) { + logger.trace("Request headers: {}", headers); + } } - } - @Override - public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - context.proceed(); - if (logger.isTraceEnabled()) { - logRequestBody(context); + private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { + OutputStream entityStream = requestContext.getEntityStream(); + LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); + requestContext.setEntityStream(loggingOutputStream); + requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); } - } - - private void logUrl(ClientRequestContext requestContext) { - String method = requestContext.getMethod(); - URI uri = requestContext.getUri(); - logger.debug(method + " " + uri.toString()); - } - - private void logHeaders(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getStringHeaders(); - if (headers != null) { - logger.trace("Request headers: " + headers.toString()); + + private void logResponseBody(ClientResponseContext responseContext) throws IOException { + Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); + InputStream entityStream = responseContext.getEntityStream(); + byte[] bodyBytes = readInputStreamBytes(entityStream); + responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); + logger.trace("Response body: " + new String(bodyBytes, charset)); } - } - - private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { - OutputStream entityStream = requestContext.getEntityStream(); - LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); - requestContext.setEntityStream(loggingOutputStream); - requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); - } - - private void logResponseBody(ClientResponseContext responseContext) throws IOException { - Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); - InputStream entityStream = responseContext.getEntityStream(); - byte[] bodyBytes = readInputStreamBytes(entityStream); - responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); - logger.trace("Response body: " + new String(bodyBytes, charset)); - } - - private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = entityStream.read(buffer)) != -1) { - result.write(buffer, 0, length); + + private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = entityStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toByteArray(); } - return result.toByteArray(); - } - - private void logRequestBody(WriterInterceptorContext context) { - LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); - if (loggingOutputStream != null) { - Charset charset = MessageUtils.getCharset(context.getMediaType()); - byte[] bytes = loggingOutputStream.getBytes(); - logger.trace("Message body: " + new String(bytes, charset)); + + private void logRequestBody(WriterInterceptorContext context) { + LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); + if (loggingOutputStream != null) { + Charset charset = MessageUtils.getCharset(context.getMediaType()); + byte[] bytes = loggingOutputStream.getBytes(); + logger.trace("Message body: " + new String(bytes, charset)); + } } - } - public static class LoggingOutputStream extends FilterOutputStream { + public static class LoggingOutputStream extends FilterOutputStream { - private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - public LoggingOutputStream(OutputStream out) { - super(out); - } + public LoggingOutputStream(OutputStream out) { + super(out); + } - @Override - public void write(byte[] b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } + @Override + public void write(byte[] b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } - @Override - public void write(int b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } + @Override + public void write(int b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } - public byte[] getBytes() { - return byteArrayOutputStream.toByteArray(); + public byte[] getBytes() { + return byteArrayOutputStream.toByteArray(); + } } - } } diff --git a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java index 0672cc61..efd280bc 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java @@ -40,7 +40,8 @@ public class SessionStatusPoller { private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - private SmartIdConnector connector; + + private final SmartIdConnector connector; private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; private long pollingSleepTimeout = 1L; diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index bb6bdcf9..dcda6d34 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -46,6 +46,8 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; + +import java.io.Serial; import java.net.URI; import java.util.concurrent.TimeUnit; @@ -53,7 +55,11 @@ public class SmartIdRestConnector implements SmartIdConnector { + @Serial + private static final long serialVersionUID = 43L; + private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); + private static final String SESSION_STATUS_URI = "/session/{sessionId}"; private static final String CERTIFICATE_CHOICE_BY_DOCUMENT_NUMBER_PATH = "/certificatechoice/document/{documentNumber}"; @@ -65,12 +71,11 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String AUTHENTICATE_BY_DOCUMENT_NUMBER_PATH = "/authentication/document/{documentNumber}"; private static final String AUTHENTICATE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER = "/authentication/etsi/{semanticsIdentifier}"; - private String endpointUrl; + private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; private TimeUnit sessionStatusResponseSocketOpenTimeUnit; private long sessionStatusResponseSocketOpenTimeValue; - private static final long serialVersionUID = 42L; private transient SSLContext sslContext; public SmartIdRestConnector(String endpointUrl) { @@ -102,7 +107,6 @@ public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundEx logger.warn("Session " + request + " not found: " + e.getMessage()); throw new SessionNotFoundException(); } - } @Override diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index 6653621c..1fddf112 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,20 +26,19 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static ee.sk.smartid.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; +import static ee.sk.smartid.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; import java.io.Serializable; -import static ee.sk.smartid.rest.dao.InteractionFlow.*; +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.exception.permanent.SmartIdClientException; @JsonInclude(JsonInclude.Include.NON_NULL) public class Interaction implements Serializable { - private static final Logger logger = LoggerFactory.getLogger(Interaction.class); - private InteractionFlow type; private String displayText60; diff --git a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java index 300d9263..672800e0 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java @@ -35,7 +35,7 @@ public enum InteractionFlow { VERIFICATION_CODE_CHOICE("verificationCodeChoice"), CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); - private String code; + private final String code; InteractionFlow(String code) { this.code = code; diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index b607ee9c..ee798318 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -1,5 +1,31 @@ package ee.sk.smartid.rest.dao; +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java index b0527e18..f1faadf5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java @@ -30,7 +30,7 @@ public class SemanticsIdentifier implements Serializable { - protected String identifier; + private final String identifier; public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { this.identifier = "" + identityType + countryCode + "-" + identityNumber; diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 0297b0b5..e0235ee2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -94,7 +94,7 @@ public void setInteractionFlowUsed(String interactionFlowUsed) { /** * IP-address of the device running the App. - * + *

* Present only if withShareMdClientIpAddress() was specified with the request * Also, the RelyingParty must be subscribed for the service. * Also, the data must be available (e.g. not present in case state is TIMEOUT). diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java index 1c3c0e0e..b3286e61 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java @@ -31,7 +31,7 @@ public class SessionStatusRequest implements Serializable { - private String sessionId; + private final String sessionId; private TimeUnit responseSocketOpenTimeUnit; private long responseSocketOpenTimeValue; @@ -45,10 +45,10 @@ public String getSessionId() { /** * Request long poll timeout value. If not provided, a default is used. - * + *

* This parameter is used for a long poll method, meaning the request method might not return until a timeout expires * set by this parameter. - * + *

* Caller can tune the request parameters inside the bounds set by service operator. * * @param timeUnit time unit of how much time a network request socket should be kept open. diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index 50be337e..ce0aa48f 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,13 +26,6 @@ * #L% */ -import ee.sk.smartid.AuthenticationIdentity; -import org.bouncycastle.asn1.*; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x509.Extension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.cert.X509Certificate; @@ -43,18 +36,35 @@ import java.util.Enumeration; import java.util.Optional; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.DLSet; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.Extension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; + public class CertificateAttributeUtil { private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); /** * Get Date-of-birth (DoB) from a specific certificate header (if present). - * + *

* NB! This attribute may be present on some newer certificates (since ~ May 2021) but not all. * - * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. - * * @param x509Certificate Certificate to read the date-of-birth attribute from * @return Person date of birth or null if this attribute is not set. + * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. */ public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { Optional dateOfBirth = getDateOfBirthCertificateAttribute(x509Certificate); @@ -62,15 +72,29 @@ public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { return dateOfBirth.map(date -> date.toInstant().atZone(ZoneOffset.UTC).toLocalDate()).orElse(null); } - private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { + /** + * Get value of attribute in X.500 principal. + * + * @param distinguishedName X.500 distinguished name using the format defined in RFC 2253. + * @param oid Object Identifier (OID) of the attribute to extract + * @return Attribute value + */ + public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { + var x500name = new X500Name(distinguishedName); + RDN[] rdns = x500name.getRDNs(oid); + if (rdns.length == 0) { + return Optional.empty(); + } + return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); + } + + private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { try { return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); - } - catch (IOException | ClassCastException e) { + } catch (IOException | ClassCastException e) { logger.info("Could not extract date-of-birth from certificate attribute. It seems the attribute does not exist in certificate."); - } - catch (ParseException e) { + } catch (ParseException e) { logger.warn("Date of birth field existed in certificate but failed to parse the value"); } return Optional.empty(); @@ -91,8 +115,7 @@ private static Date getDateOfBirthFromAttributeInternal(X509Certificate x509Cert while (objects.hasMoreElements()) { Object param = objects.nextElement(); - if (param instanceof ASN1ObjectIdentifier) { - ASN1ObjectIdentifier id = (ASN1ObjectIdentifier) param; + if (param instanceof ASN1ObjectIdentifier id) { if (id.equals(BCStyle.DATE_OF_BIRTH) && objects.hasMoreElements()) { Object nextElement = objects.nextElement(); diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index 5453ec45..f1f77da8 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -35,6 +35,8 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class NationalIdentityNumberUtil { private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); @@ -44,7 +46,7 @@ public class NationalIdentityNumberUtil { /** * Detect date-of-birth from a Baltic national identification number if possible or return null. - * + *

* This method always returns the value for all Estonian and Lithuanian national identification numbers. * * It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 @@ -63,36 +65,22 @@ public class NationalIdentityNumberUtil { public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { String identityNumber = authenticationIdentity.getIdentityNumber(); - switch ( authenticationIdentity.getCountry().toUpperCase()) { - case "EE": - case "LT": - return parseEeLtDateOfBirth(identityNumber); - case "LV": - return parseLvDateOfBirth(identityNumber); - default: - return null; - } + return switch (authenticationIdentity.getCountry().toUpperCase()) { + case "EE", "LT" -> parseEeLtDateOfBirth(identityNumber); + case "LV" -> parseLvDateOfBirth(identityNumber); + default -> null; + }; } public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); - switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { - case "1": - case "2": - birthDate = "18" + birthDate; - break; - case "3": - case "4": - birthDate = "19" + birthDate; - break; - case "5": - case "6": - birthDate = "20" + birthDate; - break; - default: - throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); - } + birthDate = switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { + case "1", "2" -> "18" + birthDate; + case "3", "4" -> "19" + birthDate; + case "5", "6" -> "20" + birthDate; + default -> throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); + }; try { return LocalDate.parse(birthDate, DATE_FORMATTER_YYYY_MM_DD); @@ -103,7 +91,7 @@ public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { String birthDay = lvNationalIdentityNumber.substring(0, 2); - if ("32".equals(birthDay)) { + if (isNonParsableLVPersonCodePrefix(birthDay)) { logger.debug("Person has newer type of Latvian ID-code that does not carry birthdate info"); return null; } @@ -111,21 +99,12 @@ public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { String birthMonth = lvNationalIdentityNumber.substring(2, 4); String birthYearTwoDigit = lvNationalIdentityNumber.substring(4, 6); String century = lvNationalIdentityNumber.substring(7, 8); - String birthDateYyyyMmDd; - - switch (century) { - case "0": - birthDateYyyyMmDd = "18" + (birthYearTwoDigit + birthMonth + birthDay); - break; - case "1": - birthDateYyyyMmDd = "19" + (birthYearTwoDigit + birthMonth + birthDay); - break; - case "2": - birthDateYyyyMmDd = "20" + (birthYearTwoDigit + birthMonth + birthDay); - break; - default: - throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); - } + String birthDateYyyyMmDd = switch (century) { + case "0" -> "18" + (birthYearTwoDigit + birthMonth + birthDay); + case "1" -> "19" + (birthYearTwoDigit + birthMonth + birthDay); + case "2" -> "20" + (birthYearTwoDigit + birthMonth + birthDay); + default -> throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); + }; try { return LocalDate.parse(birthDateYyyyMmDd, DATE_FORMATTER_YYYY_MM_DD); @@ -134,4 +113,9 @@ public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { } } + private static boolean isNonParsableLVPersonCodePrefix(String prefix) { + Pattern pattern = Pattern.compile("3[2-9]"); + Matcher matcher = pattern.matcher(prefix); + return matcher.matches(); + } } diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index 5d63daa8..bc8e230f 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -29,11 +29,11 @@ public class StringUtil { public static boolean isNotEmpty(final CharSequence cs) { - return cs != null && cs.length() > 0; + return cs != null && !cs.isEmpty(); } public static boolean isEmpty(final CharSequence cs) { - return cs == null || cs.length() == 0; + return cs == null || cs.isEmpty(); } } diff --git a/src/test/java/ee/sk/CertificateUtil.java b/src/test/java/ee/sk/CertificateUtil.java new file mode 100644 index 00000000..59f0275e --- /dev/null +++ b/src/test/java/ee/sk/CertificateUtil.java @@ -0,0 +1,56 @@ +package ee.sk; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import ee.sk.smartid.CertificateParser; + +public final class CertificateUtil { + + private CertificateUtil() { + } + + public static byte[] getX509CertificateBytes(String base64Certificate) { + String caCertificateInPem = CertificateParser.BEGIN_CERT + "\n" + base64Certificate + "\n" + CertificateParser.END_CERT; + return caCertificateInPem.getBytes(); + } + + public static X509Certificate getX509Certificate(byte[] certificateBytes) throws CertificateException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + } + + public static X509Certificate getX509Certificate(String base64Certificate) throws CertificateException { + byte[] certificateBytes = getX509CertificateBytes(base64Certificate); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + } +} diff --git a/src/test/java/ee/sk/FileUtil.java b/src/test/java/ee/sk/FileUtil.java new file mode 100644 index 00000000..0fd31e3c --- /dev/null +++ b/src/test/java/ee/sk/FileUtil.java @@ -0,0 +1,55 @@ +package ee.sk; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public final class FileUtil { + + private FileUtil() { + } + + public static String readFileToString(String fileName) { + return new String(readFileBytes(fileName), StandardCharsets.UTF_8); + } + + private static byte[] readFileBytes(String fileName) { + try { + ClassLoader classLoader = FileUtil.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + return Files.readAllBytes(Paths.get(resource.toURI())); + } catch (Exception e) { + throw new RuntimeException("Exception: " + e.getMessage(), e); + } + } +} diff --git a/src/test/java/ee/sk/SmartIdDemoCondition.java b/src/test/java/ee/sk/SmartIdDemoCondition.java new file mode 100644 index 00000000..ed0df541 --- /dev/null +++ b/src/test/java/ee/sk/SmartIdDemoCondition.java @@ -0,0 +1,52 @@ +package ee.sk; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SmartIdDemoCondition implements ExecutionCondition { + + /** + * Allows switching off tests going against smart-id demo env. + * This is sometimes needed if the test data in smart-id is temporarily broken. + */ + private static final boolean TEST_AGAINST_SMART_ID_DEMO = true; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional element = context.getElement(); + if (element.isPresent() && element.get().isAnnotationPresent(SmartIdDemoIntegrationTest.class) && !TEST_AGAINST_SMART_ID_DEMO) { + return ConditionEvaluationResult.disabled("Running against Smart-ID demo is turned off"); + } + return ConditionEvaluationResult.enabled("Running against Smart-ID demo is turned on"); + } +} diff --git a/src/test/java/ee/sk/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/SmartIdDemoIntegrationTest.java new file mode 100644 index 00000000..e3ae6785 --- /dev/null +++ b/src/test/java/ee/sk/SmartIdDemoIntegrationTest.java @@ -0,0 +1,40 @@ +package ee.sk; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target({ElementType.TYPE, ElementType.METHOD}) // Can be applied to classes or methods +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SmartIdDemoCondition.class) +public @interface SmartIdDemoIntegrationTest { +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java index 49b583d7..a8e5012c 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,14 +26,15 @@ * #L% */ -import org.junit.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.Test; + public class AuthenticationIdentityTest { - @SuppressWarnings( "deprecation" ) + @SuppressWarnings("deprecation") @Test public void setSurName() { AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); @@ -42,7 +43,7 @@ public void setSurName() { assertThat(authenticationIdentity.getSurname(), is("surname1")); } - @SuppressWarnings( "deprecation" ) + @SuppressWarnings("deprecation") @Test public void getSurName() { AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); @@ -66,6 +67,4 @@ public void setIdentityCode() { assertThat(authenticationIdentity.getIdentityNumber(), is("identityCode")); } - - } diff --git a/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java index 8f85fa66..479ef871 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,596 +26,603 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.*; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.*; -import org.apache.commons.codec.binary.Base64; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import static ee.sk.smartid.DummyData.createSessionEndResult; +import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; +import static ee.sk.smartid.DummyData.createUserSelectedWrongVerificationCode; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.cert.CertificateEncodingException; import java.util.Collections; -import static ee.sk.smartid.DummyData.*; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.*; +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -public class AuthenticationRequestBuilderTest { +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnectorSpy; +import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.Capability; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; - private SmartIdConnectorSpy connector; - private AuthenticationRequestBuilder builder; - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @Before - public void setUp() { - connector = new SmartIdConnectorSpy(); - connector.authenticationSessionResponseToRespond = createDummyAuthenticationSessionResponse(); - connector.sessionStatusToRespond = createDummySessionStatusResponse(); - builder = new AuthenticationRequestBuilder(connector, new SessionStatusPoller(connector)); - } - - @Test - public void authenticateWithDocumentNumberAndGeneratedHash() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticateWithHash() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities("ADVANCED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==", "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticate_usingSemanticsIdentifier() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withSemanticsIdentifier(new SemanticsIdentifier("IDCCZ-1234567890")) - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticate_usingSemanticsIdentifierAsString() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticateWithoutCertificateLevel_shouldPass() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), null); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withShareMdClientIpAddressTrue() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(true) - .authenticate(); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.authenticationSessionRequestUsed.getRequestProperties()); - assertTrue("requestProperties.shareMdClientIpAddress must be true", - connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withShareMdClientIpAddressFalse() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(false) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.authenticationSessionRequestUsed.getRequestProperties()); - - assertFalse("requestProperties.shareMdClientIpAddress must be false", - connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withoutDocumentNumber_withoutSemanticsIdentifier_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either documentNumber or semanticsIdentifier must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticate_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Exactly one of documentNumber or semanticsIdentifier must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticate_withoutHashAndWithoutDataToSign_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticateWithHash_withoutHashType_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticateWithHash_withoutHash_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticateWithoutRelyingPartyUuid_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Parameter relyingPartyUUID must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticateWithoutRelyingPartyName_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Parameter relyingPartyName must be set"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - } - - @Test - public void authenticate_withTooLongNonce_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .authenticate(); - } - - @Test - public void authenticate_missingAllowedInteractionOrder_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Missing or empty mandatory parameter allowedInteractionsOrder"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .authenticate(); - } - - @Test - public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText60 must not be longer than 60 characters"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) - ) - .authenticate(); - } - - @Test - public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText60 must not be longer than 60 characters"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) - ) - .authenticate(); - } - - @Test - public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText200 must not be longer than 200 characters"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .authenticate(); - } - - @Test - public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText200 must not be longer than 200 characters"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .authenticate(); - } - - @Test - public void authenticate_userRefused_shouldThrowException() { - expectedException.expect(UserRefusedException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userRefusedCertChoice_shouldThrowException() { - expectedException.expect(UserRefusedCertChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userRefusedDisplayTextAndPin_shouldThrowException() { - expectedException.expect(UserRefusedDisplayTextAndPinException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userRefusedVerificationChoice_shouldThrowException() { - expectedException.expect(UserRefusedVerificationChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userRefusedConfirmationMessage_shouldThrowException() { - expectedException.expect(UserRefusedConfirmationMessageException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { - expectedException.expect(UserRefusedConfirmationMessageWithVerificationChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_userSelectedWrongVerificationCode_shouldThrowException() { - expectedException.expect(UserSelectedWrongVerificationCodeException.class); - - connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_resultMissingInResponse_shouldThrowException() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - - connector.sessionStatusToRespond.setResult(null); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_signatureMissingInResponse_shouldThrowException() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - - connector.sessionStatusToRespond.setSignature(null); - makeAuthenticationRequest(); - } - - @Test - public void authenticate_certificateMissingInResponse_shouldThrowException() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - - connector.sessionStatusToRespond.setCert(null); - makeAuthenticationRequest(); - } - - private void assertCorrectAuthenticationRequestMadeWithDocumentNumber(String expectedHashToSignInBase64, String expectedCertificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); - } - - private void assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(String expectedHashToSignInBase64, String expectedCertificateLevel) { - assertEquals("IDCCZ-1234567890", connector.semanticsIdentifierUsed.getIdentifier()); - assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertAuthenticationResponseCorrect(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) throws CertificateEncodingException { - assertNotNull(authenticationResponse); - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); - assertEquals("c2FtcGxlIHNpZ25hdHVyZQ0K", authenticationResponse.getSignatureValueInBase64()); - assertEquals("sha512WithRSAEncryption", authenticationResponse.getAlgorithmName()); - assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(authenticationResponse.getCertificate().getEncoded())); - assertEquals("QUALIFIED", authenticationResponse.getCertificateLevel()); - - assertThat(authenticationResponse.getInteractionFlowUsed(), is("displayTextAndPIN")); - } - - private AuthenticationSessionResponse createDummyAuthenticationSessionResponse() { - AuthenticationSessionResponse response = new AuthenticationSessionResponse(); - response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return response; - } - - private SessionStatus createDummySessionStatusResponse() { - SessionSignature signature = new SessionSignature(); - signature.setValue("c2FtcGxlIHNpZ25hdHVyZQ0K"); - signature.setAlgorithm("sha512WithRSAEncryption"); - - SessionCertificate certificate = new SessionCertificate(); - certificate.setCertificateLevel("QUALIFIED"); - certificate.setValue(DummyData.CERTIFICATE); - - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setResult(createSessionEndResult()); - status.setSignature(signature); - status.setCert(certificate); - status.setInteractionFlowUsed("displayTextAndPIN"); - status.setDeviceIpAddress("4.4.4.4"); - return status; - } - - private void makeAuthenticationRequest() { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?"))) - .authenticate(); - } +public class AuthenticationRequestBuilderTest { + private SmartIdConnectorSpy connector; + private AuthenticationRequestBuilder builder; + + @BeforeEach + public void setUp() { + connector = new SmartIdConnectorSpy(); + connector.authenticationSessionResponseToRespond = createDummyAuthenticationSessionResponse(); + connector.sessionStatusToRespond = createDummySessionStatusResponse(); + builder = new AuthenticationRequestBuilder(connector, new SessionStatusPoller(connector)); + } + + @Test + public void authenticateWithDocumentNumberAndGeneratedHash() throws Exception { + AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); + } + + @Test + public void authenticateWithHash() throws Exception { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withCapabilities("ADVANCED") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithDocumentNumber("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==", "QUALIFIED"); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + } + + @Test + public void authenticate_usingSemanticsIdentifier() throws Exception { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withSemanticsIdentifier(new SemanticsIdentifier("IDCCZ-1234567890")) + .withCapabilities(Capability.ADVANCED) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + } + + @Test + public void authenticate_usingSemanticsIdentifierAsString() throws Exception { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withSemanticsIdentifierAsString("IDCCZ-1234567890") + .withCapabilities(Capability.ADVANCED) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + } + + @Test + public void authenticateWithoutCertificateLevel_shouldPass() throws Exception { + AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), null); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); + } + + @Test + public void authenticate_withShareMdClientIpAddressTrue() throws Exception { + AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .withShareMdClientIpAddress(true) + .authenticate(); + + assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + assertTrue(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); + + assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); + } + + @Test + public void authenticate_withShareMdClientIpAddressFalse() throws Exception { + AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); + + SmartIdAuthenticationResponse authenticationResponse = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .withShareMdClientIpAddress(false) + .authenticate(); + + assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); + + assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + + assertFalse(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); + + assertCorrectSessionRequestMade(); + assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); + } + + @Test + public void authenticate_withoutDocumentNumber_withoutSemanticsIdentifier_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withSemanticsIdentifierAsString("IDCCZ-1234567890") + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Exactly one of documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_withoutHashAndWithoutDataToSign_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, + () -> builder.withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate()); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticateWithHash_withoutHashType_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticateWithHash_withoutHash_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withAuthenticationHash(authenticationHash) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticateWithoutRelyingPartyUuid_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Parameter relyingPartyUUID must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticateWithoutRelyingPartyName_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .authenticate(); + }); + assertEquals("Parameter relyingPartyName must be set", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_withTooLongNonce_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") + .authenticate(); + }); + assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_missingAllowedInteractionOrder_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .authenticate(); + }); + assertEquals("Missing or empty mandatory parameter allowedInteractionsOrder", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) + ) + .authenticate(); + }); + assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) + ) + .authenticate(); + }); + assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) + ) + .authenticate(); + }); + assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) + ) + .authenticate(); + }); + assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_userRefused_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userRefusedCertChoice_shouldThrowException() { + assertThrows(UserRefusedCertChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userRefusedDisplayTextAndPin_shouldThrowException() { + assertThrows(UserRefusedDisplayTextAndPinException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userRefusedVerificationChoice_shouldThrowException() { + assertThrows(UserRefusedVerificationChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userRefusedConfirmationMessage_shouldThrowException() { + assertThrows(UserRefusedConfirmationMessageException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { + assertThrows(UserRefusedConfirmationMessageWithVerificationChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_userSelectedWrongVerificationCode_shouldThrowException() { + assertThrows(UserSelectedWrongVerificationCodeException.class, () -> { + connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_resultMissingInResponse_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.setResult(null); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_signatureMissingInResponse_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.setSignature(null); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_certificateMissingInResponse_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.setCert(null); + makeAuthenticationRequest(); + }); + } + + private void assertCorrectAuthenticationRequestMadeWithDocumentNumber(String expectedHashToSignInBase64, String expectedCertificateLevel) { + assertEquals("PNOEE-31111111111", connector.documentNumberUsed); + assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); + assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); + assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); + assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); + assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); + } + + private void assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(String expectedHashToSignInBase64, String expectedCertificateLevel) { + assertEquals("IDCCZ-1234567890", connector.semanticsIdentifierUsed.getIdentifier()); + assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); + assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); + assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); + assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); + assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); + } + + private void assertCorrectSessionRequestMade() { + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); + } + + private void assertAuthenticationResponseCorrect(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) throws CertificateEncodingException { + assertNotNull(authenticationResponse); + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); + assertEquals("c2FtcGxlIHNpZ25hdHVyZQ0K", authenticationResponse.getSignatureValueInBase64()); + assertEquals("sha512WithRSAEncryption", authenticationResponse.getAlgorithmName()); + assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(authenticationResponse.getCertificate().getEncoded())); + assertEquals("QUALIFIED", authenticationResponse.getCertificateLevel()); + + assertThat(authenticationResponse.getInteractionFlowUsed(), is("displayTextAndPIN")); + } + + private AuthenticationSessionResponse createDummyAuthenticationSessionResponse() { + AuthenticationSessionResponse response = new AuthenticationSessionResponse(); + response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + return response; + } + + private SessionStatus createDummySessionStatusResponse() { + SessionSignature signature = new SessionSignature(); + signature.setValue("c2FtcGxlIHNpZ25hdHVyZQ0K"); + signature.setAlgorithm("sha512WithRSAEncryption"); + + SessionCertificate certificate = new SessionCertificate(); + certificate.setCertificateLevel("QUALIFIED"); + certificate.setValue(DummyData.CERTIFICATE); + + SessionStatus status = new SessionStatus(); + status.setState("COMPLETE"); + status.setResult(createSessionEndResult()); + status.setSignature(signature); + status.setCert(certificate); + status.setInteractionFlowUsed("displayTextAndPIN"); + status.setDeviceIpAddress("4.4.4.4"); + return status; + } + + private void makeAuthenticationRequest() { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + authenticationHash.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?"))) + .authenticate(); + } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 3b18f8c2..b7feab18 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,342 +26,332 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import org.apache.commons.codec.binary.Base64; -import org.hamcrest.core.StringContains; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import java.io.ByteArrayInputStream; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.LocalDate; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; +import javax.security.auth.x500.X500Principal; + +import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.CertificateUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; public class AuthenticationResponseValidatorTest { - - private static final String VALID_SIGNATURE_IN_BASE64 = "F84UserdWKmmsZeu5trpMT+yhqZ3aMYMhQatSrRkq3TrYWS/xaE1yzmuzNdYXELs3ZGURuXsePfPKFBvc+PTU7oRHT8dxq3zuAqhDZO8VN5iWKpjF0LTwcA4sO6+uw5hXewG/e8I/CutyYlfcobFvLIqXvXXLl2fcAeQbMvKhj/6yuwwz3b7INVDKQnz/8y+v5/XXBFnlniNJNx7d4Kk+IL7r3DMzttKrldOUzUOuIVb6sdBcrg0+LWClMIt6nCP+T006iRruGqvPpbIsEOs2JIuZo3eh7j6nX2xtMzzgd87BDUzHIFJTj8ZVQu/Yp5A4O3iL2k3E+oOX/5wQkleC6sJ94M6kPliK0LCBv7xcMUmSnwPR3ZjNCX315F21k+ikwK6JlXxBS9pvfLNi2574112yBCq4hB7VKRdORSja9XF4jhoL/rbqisuHRqIMCg3weK6dprSJB1+3pyDGzYPLsV+6RnAb958e/0A7Mq1wg4qjjlqpn32CifoGbwABjUzBhOJC/IFp5ftVQfq3KPLPviyHZN8uIuwwDfI3A9PIOOqu5jt31G777DKGW1xMwd3yRErZ2fbNbNAKjpjeNQtQmS0rcX+l0efBMe4PCmRpT3Sv0i/vNkTlZfqB2NkVSLzTevDt0N1UU+N6u4v5ZEmuEqtoXGWT4ZRlUTUc1oUG8w="; - private static final String INVALID_SIGNATURE_IN_BASE64 = "XDzm10vKbvMMKv+o7i/Sz726hbcKPiWxtmP8Wc68v5BnJOp+STDhyq18CEAyIG/ucmlRi/TtTFn+7r6jNEczZ+2wIlDq7J8WJ3TKbAiCUUAoFccon2fqXAZHGceO/pRfrEbVsy6Oh9HodOwr/7A1a46JCCif9w/1ZE84Tm1RVsJHSkBdKYFOPTCEbN2AXZXDU9qshIyjLHrIyZ3ve6ay6L2xCyK1VOY6y3zsavzxd2CjAkvk9l1MrMLKOoI4lHXmIqDTr1I5ixMZ/g05aua0AHGE/cOp1XRj5lRJW48kjISidH9lPdnEHTKZJ6SFc/ZpZOYt7W+BNMb2dcvgOWrRXICPy0KfAh6gRAJIOUe6kPhIqvGnZ450fX1eO5wd957a1Tjlw6+h7AGf1YFYciLBpC+D3k/E8VDJUoicJBfzGFjEhd4xJYFGw3ZqUWr7dF/6LLSBpL1B87kHhsFhpn+3h0AWJaSqkD1DW3upSdlTZOV+IqoPlTMzV6HJn1yOGrg+yWBiCX1Xs7NbbMveyg/7E/wxVYOaaXGeXp4yaLxS1YJMu0PiQByvhZyarEPWEc6imlmg6LKUYzu6rklcQL7dW8xUW7n6gLx+Jyh+4KVyom968LtjC8zXCkL+VkiWRQIbOx6+k/q+4/aR9tG9rgjMCSV5kYn+kLRGfNA8eHp891c="; + private static final String VALID_SIGNATURE_IN_BASE64 = "F84UserdWKmmsZeu5trpMT+yhqZ3aMYMhQatSrRkq3TrYWS/xaE1yzmuzNdYXELs3ZGURuXsePfPKFBvc+PTU7oRHT8dxq3zuAqhDZO8VN5iWKpjF0LTwcA4sO6+uw5hXewG/e8I/CutyYlfcobFvLIqXvXXLl2fcAeQbMvKhj/6yuwwz3b7INVDKQnz/8y+v5/XXBFnlniNJNx7d4Kk+IL7r3DMzttKrldOUzUOuIVb6sdBcrg0+LWClMIt6nCP+T006iRruGqvPpbIsEOs2JIuZo3eh7j6nX2xtMzzgd87BDUzHIFJTj8ZVQu/Yp5A4O3iL2k3E+oOX/5wQkleC6sJ94M6kPliK0LCBv7xcMUmSnwPR3ZjNCX315F21k+ikwK6JlXxBS9pvfLNi2574112yBCq4hB7VKRdORSja9XF4jhoL/rbqisuHRqIMCg3weK6dprSJB1+3pyDGzYPLsV+6RnAb958e/0A7Mq1wg4qjjlqpn32CifoGbwABjUzBhOJC/IFp5ftVQfq3KPLPviyHZN8uIuwwDfI3A9PIOOqu5jt31G777DKGW1xMwd3yRErZ2fbNbNAKjpjeNQtQmS0rcX+l0efBMe4PCmRpT3Sv0i/vNkTlZfqB2NkVSLzTevDt0N1UU+N6u4v5ZEmuEqtoXGWT4ZRlUTUc1oUG8w="; + + private static final String INVALID_SIGNATURE_IN_BASE64 = "XDzm10vKbvMMKv+o7i/Sz726hbcKPiWxtmP8Wc68v5BnJOp+STDhyq18CEAyIG/ucmlRi/TtTFn+7r6jNEczZ+2wIlDq7J8WJ3TKbAiCUUAoFccon2fqXAZHGceO/pRfrEbVsy6Oh9HodOwr/7A1a46JCCif9w/1ZE84Tm1RVsJHSkBdKYFOPTCEbN2AXZXDU9qshIyjLHrIyZ3ve6ay6L2xCyK1VOY6y3zsavzxd2CjAkvk9l1MrMLKOoI4lHXmIqDTr1I5ixMZ/g05aua0AHGE/cOp1XRj5lRJW48kjISidH9lPdnEHTKZJ6SFc/ZpZOYt7W+BNMb2dcvgOWrRXICPy0KfAh6gRAJIOUe6kPhIqvGnZ450fX1eO5wd957a1Tjlw6+h7AGf1YFYciLBpC+D3k/E8VDJUoicJBfzGFjEhd4xJYFGw3ZqUWr7dF/6LLSBpL1B87kHhsFhpn+3h0AWJaSqkD1DW3upSdlTZOV+IqoPlTMzV6HJn1yOGrg+yWBiCX1Xs7NbbMveyg/7E/wxVYOaaXGeXp4yaLxS1YJMu0PiQByvhZyarEPWEc6imlmg6LKUYzu6rklcQL7dW8xUW7n6gLx+Jyh+4KVyom968LtjC8zXCkL+VkiWRQIbOx6+k/q+4/aR9tG9rgjMCSV5kYn+kLRGfNA8eHp891c="; + + private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; + private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; + private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; + + private static final String HASH_TO_SIGN_IN_BASE64 = "pcWJTcOvmk5Xcvyfrit9SF55S3qU+NfEEVxg4fVf+GdxMN0W2wSpJVivcf91IG+Ji3aCGlNN8p5scBEn6mgUOg=="; + + private AuthenticationResponseValidator validator; + + @BeforeEach + public void setUp() { + validator = new AuthenticationResponseValidator(); + } + + @Test + public void validate() { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + AuthenticationIdentity authenticationIdentity = validator.validate(response); + + assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void validate_invalidSignatureValue() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setSignatureValueInBase64("invalid"); + + validator.validate(response); + }); + + assertEquals("Failed to verify validity of signature returned by Smart-ID", unprocessableSmartIdResponseException.getMessage()); + } + + @Test + public void validationReturnsValidAuthenticationResult_whenEndResultLowerCase_shouldPass() { + + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setEndResult("ok"); + AuthenticationIdentity authenticationIdentity = validator.validate(response); + + assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void validationReturnsInvalidAuthenticationResult_whenEndResultNotOk() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidationResponseWithInvalidEndResult(); + validator.validate(response); + }); + assertEquals("Smart-ID API returned end result code 'NOT OK'", unprocessableSmartIdResponseException.getMessage()); + } + + @Test + public void validationReturnsInvalidAuthenticationResult_whenSignatureVerificationFails() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidationResponseWithInvalidSignature(); + validator.validate(response); + }); + assertEquals("Failed to verify validity of signature returned by Smart-ID", unprocessableSmartIdResponseException.getMessage()); + } + + @Test + public void validationReturnsInvalidAuthenticationResult_whenSignersCertExpired() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidationResponseWithExpiredCertificate(); + validator.validate(response); + }); + assertEquals("Signer's certificate has expired", unprocessableSmartIdResponseException.getMessage()); + } + + @Test + public void validationReturnsInvalidAuthenticationResult_whenSignersCertNotTrusted() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + + AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); + validator.clearTrustedCACertificates(); + validator.addTrustedCACertificate(Base64.decodeBase64(AUTH_CERTIFICATE_EE)); + + validator.validate(response); + }); + assertEquals("Signer's certificate is not trusted", unprocessableSmartIdResponseException.getMessage()); + } + + @Test + public void validationReturnsValidAuthenticationResult_whenCertificateLevelHigherThanRequested_shouldPass() { + SmartIdAuthenticationResponse response = createValidationResponseWithHigherCertificateLevelThanRequested(); + AuthenticationIdentity authenticationIdentity = validator.validate(response); - public static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; - public static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; - public static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; - public static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; - public static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; + assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - private static final String HASH_TO_SIGN_IN_BASE64 = "pcWJTcOvmk5Xcvyfrit9SF55S3qU+NfEEVxg4fVf+GdxMN0W2wSpJVivcf91IG+Ji3aCGlNN8p5scBEn6mgUOg=="; + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void validationReturnsInvalidAuthenticationResult_whenCertificateLevelLowerThanRequested() { + var certificateLevelMismatchException = assertThrows(CertificateLevelMismatchException.class, () -> { + SmartIdAuthenticationResponse response = createValidationResponseWithLowerCertificateLevelThanRequested(); + validator.validate(response); + }); + assertEquals("Signer's certificate is below requested certificate level", certificateLevelMismatchException.getMessage()); + } + + @Test + public void testTrustedCACertificateLoadingInPEMFormat() throws CertificateException { + byte[] caCertificateInPem = CertificateUtil.getX509CertificateBytes(AUTH_CERTIFICATE_EE); + + AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); + validator.clearTrustedCACertificates(); + validator.addTrustedCACertificate(caCertificateInPem); + + X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE).getSubjectX500Principal(); + assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); + } + + @Test + public void testTrustedCACertificateLoadingInDERFormat() throws CertificateException { + byte[] caCertificateInDER = Base64.decodeBase64(AUTH_CERTIFICATE_EE); + + AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); + validator.clearTrustedCACertificates(); + validator.addTrustedCACertificate(caCertificateInDER); + + X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE).getSubjectX500Principal(); + assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); + } + + @Test + public void testTrustedCACertificateLoadingFromFile() throws IOException, CertificateException { + File caCertificateFile = new File(AuthenticationResponseValidatorTest.class.getResource("/trusted_certificates/TEST_of_EID-SK_2016.pem.crt").getFile()); + + AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); + validator.clearTrustedCACertificates(); + validator.addTrustedCACertificate(caCertificateFile); + + X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(Files.readAllBytes(caCertificateFile.toPath())).getSubjectX500Principal(); + assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); + } + + @Test + public void withEmptyRequestedCertificateLevel_shouldPass() { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setRequestedCertificateLevel(""); + AuthenticationIdentity authenticationIdentity = validator.validate(response); + + assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void withNullRequestedCertificateLevel_shouldPass() { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setRequestedCertificateLevel(null); + AuthenticationIdentity authenticationIdentity = validator.validate(response); + + assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } - private AuthenticationResponseValidator validator; + @Test + public void whenCertificateIsNull_ThenThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setCertificate(null); + validator.validate(response); + }); + } - @Rule - public ExpectedException expectedException = ExpectedException.none(); + @Test + public void whenSignatureIsEmpty_ThenThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setSignatureValueInBase64(""); + validator.validate(response); + }); + } - @Before - public void setUp() { - validator = new AuthenticationResponseValidator(); - } + @Test + public void whenHashTypeIsNull_ThenThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse response = createValidValidationResponse(); + response.setHashType(null); + validator.validate(response); + }); + } - @Test - public void validate() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + @Test + public void shouldConstructAuthenticationIdentityEE() throws CertificateException { + X509Certificate certificateEe = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE); - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801,1,1))); - } + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateEe); - @Test - public void validate_invalidSignatureValue() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Failed to verify validity of signature returned by Smart-ID")); + assertThat(authenticationIdentity.getIdentityNumber(), is("10101010005")); + assertThat(authenticationIdentity.getCountry(), is("EE")); + assertThat(authenticationIdentity.getGivenName(), is("DEMO")); + assertThat(authenticationIdentity.getSurname(), is("SMART-ID")); - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setSignatureValueInBase64("invalid"); - - validator.validate(response); - } + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); + } - @Test - public void validationReturnsValidAuthenticationResult_whenEndResultLowerCase_shouldPass() { + @Test + public void shouldConstructAuthenticationIdentityLV() throws CertificateException { + X509Certificate certificateLv = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV); - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setEndResult("ok"); - AuthenticationIdentity authenticationIdentity = validator.validate(response); + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLv); - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); + assertThat(authenticationIdentity.getIdentityNumber(), is("010117-21234")); + assertThat(authenticationIdentity.getCountry(), is("LV")); + assertThat(authenticationIdentity.getGivenName(), is("FORENAME-010117-21234")); + assertThat(authenticationIdentity.getSurname(), is("SURNAME-010117-21234")); - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801,1,1))); - } + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(2017, 1, 1))); + } - @Test - public void validationReturnsInvalidAuthenticationResult_whenEndResultNotOk() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Smart-ID API returned end result code 'NOT OK'")); + @Test + public void shouldConstructAuthenticationIdentityLT() throws CertificateException { + X509Certificate certificateLt = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LT); - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidEndResult(); - validator.validate(response); - } + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLt); - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignatureVerificationFails() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Failed to verify validity of signature returned by Smart-ID")); + assertThat(authenticationIdentity.getIdentityNumber(), is("36009067968")); + assertThat(authenticationIdentity.getCountry(), is("LT")); + assertThat(authenticationIdentity.getGivenName(), is("FORENAMEPNOLT-36009067968")); + assertThat(authenticationIdentity.getSurname(), is("SURNAMEPNOLT-36009067968")); - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidSignature(); - validator.validate(response); - } + assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); + assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1960, 9, 6))); + } + + private SmartIdAuthenticationResponse createValidValidationResponse() { + return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); + } + + private SmartIdAuthenticationResponse createValidationResponseWithInvalidEndResult() { + return createValidationResponse("NOT OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); + } - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignersCertExpired() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Signer's certificate has expired")); - - SmartIdAuthenticationResponse response = createValidationResponseWithExpiredCertificate(); - validator.validate(response); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignersCertNotTrusted() throws CertificateException { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage(StringContains.containsString("Signer's certificate is not trusted")); - - SmartIdAuthenticationResponse response = createValidValidationResponse(); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(Base64.decodeBase64(AUTH_CERTIFICATE_EE)); - - validator.validate(response); - } - - @Test - public void validationReturnsValidAuthenticationResult_whenCertificateLevelHigherThanRequested_shouldPass() { - SmartIdAuthenticationResponse response = createValidationResponseWithHigherCertificateLevelThanRequested(); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801,1,1))); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenCertificateLevelLowerThanRequested() { - expectedException.expect(CertificateLevelMismatchException.class); - expectedException.expectMessage(StringContains.containsString("Signer's certificate is below requested certificate level")); - - SmartIdAuthenticationResponse response = createValidationResponseWithLowerCertificateLevelThanRequested(); - validator.validate(response); - } - - @Test - public void testTrustedCACertificateLoadingInPEMFormat() throws CertificateException { - byte[] caCertificateInPem = getX509CertificateBytes(AUTH_CERTIFICATE_EE); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateInPem); - - assertEquals(getX509Certificate(caCertificateInPem).getSubjectDN(), validator.getTrustedCACertificates().get(0).getSubjectDN()); - } - - @Test - public void testTrustedCACertificateLoadingInDERFormat() throws CertificateException { - byte[] caCertificateInDER = Base64.decodeBase64(AUTH_CERTIFICATE_EE); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateInDER); - - assertEquals(getX509Certificate(caCertificateInDER).getSubjectDN(), validator.getTrustedCACertificates().get(0).getSubjectDN()); - } - - @Test - public void testTrustedCACertificateLoadingFromFile() throws IOException, CertificateException { - File caCertificateFile = new File(AuthenticationResponseValidatorTest.class.getResource("/trusted_certificates/TEST_of_EID-SK_2016.pem.crt").getFile()); + private SmartIdAuthenticationResponse createValidationResponseWithInvalidSignature() { + return createValidationResponse("OK", INVALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); + } + + private SmartIdAuthenticationResponse createValidationResponseWithLowerCertificateLevelThanRequested() { + return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "ADVANCED", "QUALIFIED"); + } + + private SmartIdAuthenticationResponse createValidationResponseWithHigherCertificateLevelThanRequested() { + return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "ADVANCED"); + } + + private SmartIdAuthenticationResponse createValidationResponseWithExpiredCertificate() { + SmartIdAuthenticationResponse response = createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); + + String expiredCertificate = "MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkGA1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYDVQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0GCSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51IShSj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+MllSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5czGP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdcDmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncvpRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhNBYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSnoJPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoAxoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdFTOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDiG7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbrrl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxMTEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y52n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAKGn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbRR1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2PMnXRUgtlnV7ykFpmkR4JcQ=="; + X509Certificate expiredX509Certificate = CertificateParser.parseX509Certificate(expiredCertificate); + response.setCertificate(expiredX509Certificate); + return response; + } + + private SmartIdAuthenticationResponse createValidationResponse(String endResult, String signatureInBase64, String certificateLevel, String requestedCertificateLevel) { + SmartIdAuthenticationResponse authenticationResponse = new SmartIdAuthenticationResponse(); + authenticationResponse.setEndResult(endResult); + authenticationResponse.setSignatureValueInBase64(signatureInBase64); + authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(AUTH_CERTIFICATE_EE)); + authenticationResponse.setSignedHashInBase64(HASH_TO_SIGN_IN_BASE64); + authenticationResponse.setHashType(HashType.SHA512); + authenticationResponse.setRequestedCertificateLevel(requestedCertificateLevel); + authenticationResponse.setCertificateLevel(certificateLevel); + return authenticationResponse; + } - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateFile); - - assertEquals(getX509Certificate(Files.readAllBytes(caCertificateFile.toPath())).getSubjectDN(), validator.getTrustedCACertificates().get(0).getSubjectDN()); - } - - @Test - public void withEmptyRequestedCertificateLevel_shouldPass() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setRequestedCertificateLevel(""); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801,1,1))); - } - - @Test - public void withNullRequestedCertificateLevel_shouldPass() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setRequestedCertificateLevel(null); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801,1,1))); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void whenCertificateIsNull_ThenThrowException() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setCertificate(null); - validator.validate(response); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void whenSignatureIsEmpty_ThenThrowException() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setSignatureValueInBase64(""); - validator.validate(response); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void whenHashTypeIsNull_ThenThrowException() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setHashType(null); - validator.validate(response); - } - - @Test - public void shouldConstructAuthenticationIdentityEE() throws CertificateException { - X509Certificate certificateEe = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_EE)); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateEe); - - assertThat(authenticationIdentity.getIdentityNumber(), is("10101010005")); - assertThat(authenticationIdentity.getCountry(), is("EE")); - assertThat(authenticationIdentity.getGivenName(), is("DEMO")); - assertThat(authenticationIdentity.getSurname(), is("SMART-ID")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void shouldConstructAuthenticationIdentityLV() throws CertificateException { - X509Certificate certificateLv = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LV)); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLv); - - assertThat(authenticationIdentity.getIdentityNumber(), is("010117-21234")); - assertThat(authenticationIdentity.getCountry(), is("LV")); - assertThat(authenticationIdentity.getGivenName(), is("FORENAME-010117-21234")); - assertThat(authenticationIdentity.getSurname(), is("SURNAME-010117-21234")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(2017, 1, 1))); - } - - @Test - public void shouldConstructAuthenticationIdentityLT() throws CertificateException { - X509Certificate certificateLt = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LT)); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLt); - - assertThat(authenticationIdentity.getIdentityNumber(), is("36009067968")); - assertThat(authenticationIdentity.getCountry(), is("LT")); - assertThat(authenticationIdentity.getGivenName(), is("FORENAMEPNOLT-36009067968")); - assertThat(authenticationIdentity.getSurname(), is("SURNAMEPNOLT-36009067968")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1960, 9, 6))); - } - - private SmartIdAuthenticationResponse createValidValidationResponse() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithInvalidEndResult() { - return createValidationResponse("NOT OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithInvalidSignature() { - return createValidationResponse("OK", INVALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithLowerCertificateLevelThanRequested() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "ADVANCED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithHigherCertificateLevelThanRequested() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "ADVANCED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithExpiredCertificate() { - SmartIdAuthenticationResponse response = createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - - String expiredCertificate = "MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkGA1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYDVQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0GCSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51IShSj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+MllSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5czGP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdcDmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncvpRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhNBYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSnoJPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoAxoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdFTOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDiG7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbrrl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxMTEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y52n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAKGn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbRR1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2PMnXRUgtlnV7ykFpmkR4JcQ=="; - X509Certificate expiredX509Certificate = CertificateParser.parseX509Certificate(expiredCertificate); - response.setCertificate(expiredX509Certificate); - return response; - } - - private SmartIdAuthenticationResponse createValidationResponse(String endResult, String signatureInBase64, String certificateLevel , String requestedCertificateLevel) { - SmartIdAuthenticationResponse authenticationResponse = new SmartIdAuthenticationResponse(); - authenticationResponse.setEndResult(endResult); - authenticationResponse.setSignatureValueInBase64(signatureInBase64); - authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(AUTH_CERTIFICATE_EE)); - authenticationResponse.setSignedHashInBase64(HASH_TO_SIGN_IN_BASE64); - authenticationResponse.setHashType(HashType.SHA512); - authenticationResponse.setRequestedCertificateLevel(requestedCertificateLevel); - authenticationResponse.setCertificateLevel(certificateLevel); - return authenticationResponse; - } - - public static byte[] getX509CertificateBytes(String base64Certificate) { - String caCertificateInPem = CertificateParser.BEGIN_CERT + "\n" + base64Certificate + "\n" + CertificateParser.END_CERT; - return caCertificateInPem.getBytes(); - } - - public static X509Certificate getX509Certificate(byte[] certificateBytes) throws CertificateException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); - } - - private void assertAuthenticationIdentityValid(AuthenticationIdentity authenticationIdentity, X509Certificate certificate) { - LdapName ln; - try { - ln = new LdapName(certificate.getSubjectDN().getName()); - } catch (InvalidNameException e) { - throw new RuntimeException(e); + private void assertAuthenticationIdentityValid(AuthenticationIdentity authenticationIdentity, X509Certificate certificate) { + String distinguishedName = certificate.getSubjectX500Principal().getName(); + var x500name = new X500Name(distinguishedName); + assertEquals(getAttribute(x500name, BCStyle.GIVENNAME), authenticationIdentity.getGivenName()); + assertEquals(getAttribute(x500name, BCStyle.SURNAME), authenticationIdentity.getSurname()); + assertEquals(getAttribute(x500name, BCStyle.SERIALNUMBER).split("-", 2)[1], authenticationIdentity.getIdentityNumber()); + assertEquals(getAttribute(x500name, BCStyle.C), authenticationIdentity.getCountry()); } - for(Rdn rdn : ln.getRdns()) { - if(rdn.getType().equalsIgnoreCase("GIVENNAME")) { - assertEquals(rdn.getValue().toString(), authenticationIdentity.getGivenName()); - } else if(rdn.getType().equalsIgnoreCase("SURNAME")) { - assertEquals(rdn.getValue().toString(), authenticationIdentity.getSurname()); - } else if(rdn.getType().equalsIgnoreCase("SERIALNUMBER")) { - assertEquals(rdn.getValue().toString().split("-", 2)[1], authenticationIdentity.getIdentityNumber()); - } else if(rdn.getType().equalsIgnoreCase("C")) { - assertEquals(rdn.getValue().toString(), authenticationIdentity.getCountry()); - } + private static String getAttribute(X500Name x500name, ASN1ObjectIdentifier oid) { + RDN[] rdns = x500name.getRDNs(oid); + return IETFUtils.valueToString(rdns[0].getFirst().getValue()); } - } } diff --git a/src/test/java/ee/sk/smartid/CertificateLevelTest.java b/src/test/java/ee/sk/smartid/CertificateLevelTest.java index 45c6aa1b..58b0b1d9 100644 --- a/src/test/java/ee/sk/smartid/CertificateLevelTest.java +++ b/src/test/java/ee/sk/smartid/CertificateLevelTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,54 +26,54 @@ * #L% */ -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.Test; public class CertificateLevelTest { - @Test - public void testBothCertificateLevelsQualified() { - String certificateLevelString = "QUALIFIED"; - CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); - assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); - } + @Test + public void testBothCertificateLevelsQualified() { + String certificateLevelString = "QUALIFIED"; + CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); + assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); + } - @Test - public void testBothCertificateLevelsAdvanced() { - String certificateLevelString = "ADVANCED"; - CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); - assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); - } + @Test + public void testBothCertificateLevelsAdvanced() { + String certificateLevelString = "ADVANCED"; + CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); + assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); + } - @Test - public void testFirstCertificateLevelHigher() { - CertificateLevel certificateLevel = new CertificateLevel("QUALIFIED"); - assertTrue(certificateLevel.isEqualOrAbove("ADVANCED")); - } + @Test + public void testFirstCertificateLevelHigher() { + CertificateLevel certificateLevel = new CertificateLevel("QUALIFIED"); + assertTrue(certificateLevel.isEqualOrAbove("ADVANCED")); + } - @Test - public void testFirstCertificateLevelLower() { - CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); - assertFalse(certificateLevel.isEqualOrAbove("QUALIFIED")); - } + @Test + public void testFirstCertificateLevelLower() { + CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); + assertFalse(certificateLevel.isEqualOrAbove("QUALIFIED")); + } - @Test - public void testFirstCertLevelUnknown() { - CertificateLevel certificateLevel = new CertificateLevel("SOME UNKNOWN LEVEL"); - assertFalse(certificateLevel.isEqualOrAbove("ADVANCED")); - } + @Test + public void testFirstCertLevelUnknown() { + CertificateLevel certificateLevel = new CertificateLevel("SOME UNKNOWN LEVEL"); + assertFalse(certificateLevel.isEqualOrAbove("ADVANCED")); + } - @Test - public void testSecondCertLevelUnknown() { - CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); - assertFalse(certificateLevel.isEqualOrAbove("SOME UNKNOWN LEVEL")); - } + @Test + public void testSecondCertLevelUnknown() { + CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); + assertFalse(certificateLevel.isEqualOrAbove("SOME UNKNOWN LEVEL")); + } - - @Test(expected = IllegalArgumentException.class) - public void certificateLevel_nullArgumentToConstructor() { - new CertificateLevel(null); - } + @Test + public void certificateLevel_nullArgumentToConstructor() { + assertThrows(IllegalArgumentException.class, () -> new CertificateLevel(null)); + } } diff --git a/src/test/java/ee/sk/smartid/CertificateParserTest.java b/src/test/java/ee/sk/smartid/CertificateParserTest.java index 304444bd..9d4b70d5 100644 --- a/src/test/java/ee/sk/smartid/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/CertificateParserTest.java @@ -26,14 +26,16 @@ * #L% */ +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + import ee.sk.smartid.exception.permanent.SmartIdClientException; -import org.junit.Test; public class CertificateParserTest { - @Test(expected = SmartIdClientException.class) + @Test public void testBothCertificateLevelsQualified() { - CertificateParser.parseX509Certificate("invalid"); + assertThrows(SmartIdClientException.class, () -> CertificateParser.parseX509Certificate("invalid")); } - } diff --git a/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java index b8621ae9..24a0abc9 100644 --- a/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,291 +26,317 @@ * #L% */ +import static ee.sk.smartid.DummyData.createSessionEndResult; +import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.cert.X509Certificate; + +import javax.security.auth.x500.X500Principal; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.rest.SessionStatusPoller; import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.security.cert.X509Certificate; - -import static ee.sk.smartid.DummyData.createSessionEndResult; -import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.*; +import ee.sk.smartid.rest.dao.Capability; +import ee.sk.smartid.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionStatus; public class CertificateRequestBuilderTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private SmartIdConnectorSpy connector; - private SessionStatusPoller sessionStatusPoller; - private CertificateRequestBuilder builder; - - @Before - public void setUp() { - connector = new SmartIdConnectorSpy(); - sessionStatusPoller = new SessionStatusPoller(connector); - connector.sessionStatusToRespond = createCertificateSessionStatusCompleteResponse(); - connector.certificateChoiceToRespond = createCertificateChoiceResponse(); - builder = new CertificateRequestBuilder(connector, sessionStatusPoller); - } - - @Test - public void getCertificate_usingSemanticsIdentifier() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifierAsString("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("QUALIFIED"); - } - - @Test - public void getCertificate_usingDocumentNumber() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withCapabilities("ADVANCED") - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateRequestMadeWithDocumentNumber("QUALIFIED"); - } - - @Test - public void getCertificate_withoutAnyIdentifier_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either documentNumber or semanticsIdentifier must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - @Test - public void getCertificate_withoutCertificateLevel() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade(null); - } - - @Test - public void getCertificate_withShareMdClientIpAddressTrue() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withShareMdClientIpAddress(true) - .fetch(); - assertCertificateResponseValid(certificate); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.certificateRequestUsed.getRequestProperties()); - assertTrue("requestProperties.shareMdClientIpAddress must be true", - connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - assertThat(certificate.getDeviceIpAddress(), is("5.5.5.5")); - - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("ADVANCED"); - } - - @Test - public void getCertificate_withShareMdClientIpAddressFalse() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withShareMdClientIpAddress(false) - .fetch(); - assertCertificateResponseValid(certificate); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.certificateRequestUsed.getRequestProperties()); - assertFalse("requestProperties.shareMdClientIpAddress must be false", - connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("ADVANCED"); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_whenIdentityOrDocumentNumberNotSet_shouldThrowException() { - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_withoutRelyingPartyUUID_shouldThrowException() { - builder - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_withoutRelyingPartyName_shouldThrowException() { - builder - .withRelyingPartyUUID("relying-party-uuid") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - @Test - public void getCertificate_withTooLongNonce_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'"); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .fetch(); - } - - @Test - public void getCertificate_withCapabilities() { - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .withCapabilities(Capability.ADVANCED) - .fetch(); - } - - @Test(expected = UserRefusedException.class) - public void getCertificate_whenUserRefuses_shouldThrowException() { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - makeCertificateRequest(); - } - - @Test(expected = UserRefusedException.class) - public void getCertificate_withDocumentNumber_whenUserRefuses_shouldThrowException() { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void getCertificate_withCertificateResponseWithoutCertificate_shouldThrowException() { - connector.sessionStatusToRespond.setCert(null); - makeCertificateRequest(); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void getCertificate_withCertificateResponseContainingEmptyCertificate_shouldThrowException() { - connector.sessionStatusToRespond.getCert().setValue(""); - makeCertificateRequest(); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void getCertificate_withCertificateResponseWithoutDocumentNumber_shouldThrowException() { - connector.sessionStatusToRespond.getResult().setDocumentNumber(null); - makeCertificateRequest(); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void getCertificate_withCertificateResponseWithBlankDocumentNumber_shouldThrowException() { - connector.sessionStatusToRespond.getResult().setDocumentNumber(""); - makeCertificateRequest(); - } - - private void assertCertificateResponseValid(SmartIdCertificate certificate) { - assertNotNull(certificate); - assertNotNull(certificate.getCertificate()); - X509Certificate cert = certificate.getCertificate(); - assertThat(cert.getSubjectDN().getName(), containsString("SERIALNUMBER=PNOEE-31111111111")); - assertEquals("QUALIFIED", certificate.getCertificateLevel()); - assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertValidCertificateChoiceRequestMade(String certificateLevel) { - assertThat(connector.semanticsIdentifierUsed.getIdentifier(), is("PNOEE-31111111111")); - - assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); - } - - private void assertValidCertificateRequestMadeWithDocumentNumber(String certificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); - } - - private SessionStatus createCertificateSessionStatusCompleteResponse() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setCert(createSessionCertificate()); - status.setResult(createSessionEndResult()); - status.setDeviceIpAddress("5.5.5.5"); - return status; - } - - private SessionCertificate createSessionCertificate() { - SessionCertificate sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(DummyData.CERTIFICATE); - return sessionCertificate; - } - - private CertificateChoiceResponse createCertificateChoiceResponse() { - CertificateChoiceResponse certificateChoiceResponse = new CertificateChoiceResponse(); - certificateChoiceResponse.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return certificateChoiceResponse; - } - - private void makeCertificateRequest() { - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch(); - } - + private SmartIdConnectorSpy connector; + private CertificateRequestBuilder builder; + + @BeforeEach + public void setUp() { + connector = new SmartIdConnectorSpy(); + SessionStatusPoller sessionStatusPoller = new SessionStatusPoller(connector); + connector.sessionStatusToRespond = createCertificateSessionStatusCompleteResponse(); + connector.certificateChoiceToRespond = createCertificateChoiceResponse(); + builder = new CertificateRequestBuilder(connector, sessionStatusPoller); + } + + @Test + public void getCertificate_usingSemanticsIdentifier() { + SmartIdCertificate certificate = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifierAsString("PNOEE-31111111111") + .withCertificateLevel("QUALIFIED") + .fetch(); + assertCertificateResponseValid(certificate); + assertCorrectSessionRequestMade(); + assertValidCertificateChoiceRequestMade("QUALIFIED"); + } + + @Test + public void getCertificate_usingDocumentNumber() { + SmartIdCertificate certificate = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withDocumentNumber("PNOEE-31111111111") + .withCertificateLevel("QUALIFIED") + .withCapabilities("ADVANCED") + .fetch(); + assertCertificateResponseValid(certificate); + assertCorrectSessionRequestMade(); + assertValidCertificateRequestMadeWithDocumentNumber("QUALIFIED"); + } + + @Test + public void getCertificate_withoutAnyIdentifier_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .fetch(); + }); + assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); + } + + @Test + public void getCertificate_withoutCertificateLevel() { + SmartIdCertificate certificate = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .fetch(); + assertCertificateResponseValid(certificate); + assertCorrectSessionRequestMade(); + assertValidCertificateChoiceRequestMade(null); + } + + @Test + public void getCertificate_withShareMdClientIpAddressTrue() { + SmartIdCertificate certificate = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .withShareMdClientIpAddress(true) + .fetch(); + assertCertificateResponseValid(certificate); + + assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + assertTrue(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); + assertThat(certificate.getDeviceIpAddress(), is("5.5.5.5")); + + assertCorrectSessionRequestMade(); + assertValidCertificateChoiceRequestMade("ADVANCED"); + } + + @Test + public void getCertificate_withShareMdClientIpAddressFalse() { + SmartIdCertificate certificate = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .withShareMdClientIpAddress(false) + .fetch(); + assertCertificateResponseValid(certificate); + + assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + assertFalse(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); + + assertCorrectSessionRequestMade(); + assertValidCertificateChoiceRequestMade("ADVANCED"); + } + + @Test + public void getCertificate_whenIdentityOrDocumentNumberNotSet_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .fetch() + ); + } + + @Test + public void getCertificate_withoutRelyingPartyUUID_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> + builder + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("QUALIFIED") + .fetch() + ); + } + + @Test + public void getCertificate_withoutRelyingPartyName_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> + builder + .withRelyingPartyUUID("relying-party-uuid") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("QUALIFIED") + .fetch() + ); + } + + @Test + public void getCertificate_withTooLongNonce_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, + () -> builder.withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("QUALIFIED") + .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") + .fetch()); + assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); + } + + @Test + public void getCertificate_withCapabilities() { + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) + .withCertificateLevel("QUALIFIED") + .withCapabilities(Capability.ADVANCED) + .fetch(); + } + + @Test + public void getCertificate_whenUserRefuses_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + makeCertificateRequest(); + }); + } + + @Test + public void getCertificate_withDocumentNumber_whenUserRefuses_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withDocumentNumber("PNOEE-31111111111") + .withCertificateLevel("QUALIFIED") + .fetch(); + }); + } + + @Test + public void getCertificate_withCertificateResponseWithoutCertificate_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.setCert(null); + makeCertificateRequest(); + }); + } + + @Test + public void getCertificate_withCertificateResponseContainingEmptyCertificate_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.getCert().setValue(""); + makeCertificateRequest(); + }); + } + + @Test + public void getCertificate_withCertificateResponseWithoutDocumentNumber_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.getResult().setDocumentNumber(null); + makeCertificateRequest(); + }); + } + + @Test + public void getCertificate_withCertificateResponseWithBlankDocumentNumber_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.getResult().setDocumentNumber(""); + makeCertificateRequest(); + }); + } + + private void assertCertificateResponseValid(SmartIdCertificate certificate) { + assertNotNull(certificate); + assertNotNull(certificate.getCertificate()); + X509Certificate cert = certificate.getCertificate(); + String serialNumber = getAttributeValue(cert.getSubjectX500Principal(), BCStyle.SERIALNUMBER); + assertEquals("PNOEE-31111111111", serialNumber); + assertEquals("QUALIFIED", certificate.getCertificateLevel()); + assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); + } + + private void assertCorrectSessionRequestMade() { + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); + } + + private void assertValidCertificateChoiceRequestMade(String certificateLevel) { + assertThat(connector.semanticsIdentifierUsed.getIdentifier(), is("PNOEE-31111111111")); + + assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); + assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); + assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); + } + + private void assertValidCertificateRequestMadeWithDocumentNumber(String certificateLevel) { + assertEquals("PNOEE-31111111111", connector.documentNumberUsed); + assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); + assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); + assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); + } + + private SessionStatus createCertificateSessionStatusCompleteResponse() { + SessionStatus status = new SessionStatus(); + status.setState("COMPLETE"); + status.setCert(createSessionCertificate()); + status.setResult(createSessionEndResult()); + status.setDeviceIpAddress("5.5.5.5"); + return status; + } + + private SessionCertificate createSessionCertificate() { + SessionCertificate sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(DummyData.CERTIFICATE); + return sessionCertificate; + } + + private CertificateChoiceResponse createCertificateChoiceResponse() { + CertificateChoiceResponse certificateChoiceResponse = new CertificateChoiceResponse(); + certificateChoiceResponse.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + return certificateChoiceResponse; + } + + private void makeCertificateRequest() { + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) + .withCertificateLevel("QUALIFIED") + .fetch(); + } + + private static String getAttributeValue(X500Principal subjectX500Principal, ASN1ObjectIdentifier oid) { + var x500name = new X500Name(subjectX500Principal.getName()); + RDN[] rdns = x500name.getRDNs(oid); + return IETFUtils.valueToString(rdns[0].getFirst().getValue()); + } } diff --git a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java index 2824bf03..67770136 100644 --- a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java @@ -34,7 +34,7 @@ public class ClientRequestHeaderFilter implements ClientRequestFilter { - private Map headersToAdd; + private final Map headersToAdd; public ClientRequestHeaderFilter(Map headersToAdd) { this.headersToAdd = headersToAdd; diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index d0a229e3..eed4a309 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,25 +26,26 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.apache.commons.codec.binary.Hex; -import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; +import org.apache.commons.codec.binary.Hex; +import org.junit.jupiter.api.Test; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; public class DigestCalculatorTest { - public static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); + private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); @Test public void calculateDigest_sha256() { byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA256); - assertThat( Hex.encodeHexString(sha512), + assertThat(Hex.encodeHexString(sha512), is("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069")); } @@ -52,7 +53,7 @@ public void calculateDigest_sha256() { public void calculateDigest_sha384() { byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA384); - assertThat( Hex.encodeHexString(sha512), + assertThat(Hex.encodeHexString(sha512), is("bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a")); } @@ -60,15 +61,12 @@ public void calculateDigest_sha384() { public void calculateDigest_sha512() { byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA512); - assertThat( Hex.encodeHexString(sha512), + assertThat(Hex.encodeHexString(sha512), is("861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8")); } - - @Test(expected = UnprocessableSmartIdResponseException.class) + @Test public void calculateDigest_nullHashType() { - DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null); - + assertThrows(UnprocessableSmartIdResponseException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); } - } diff --git a/src/test/java/ee/sk/smartid/DummyData.java b/src/test/java/ee/sk/smartid/DummyData.java index a4b59658..2cd0966d 100644 --- a/src/test/java/ee/sk/smartid/DummyData.java +++ b/src/test/java/ee/sk/smartid/DummyData.java @@ -31,7 +31,7 @@ public class DummyData { - public static String CERTIFICATE = "MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTYxMjA5MTYyNDU2WhcNMTkxMjA5MTYyNDU2WjCBvzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMS0wKwYDVQQDDCRFTEZSSUlEQSxNQU5JVkFMREUsUE5PRUUtMzExMTExMTExMTExETAPBgNVBAQMCEVMRlJJSURBMRIwEAYDVQQqDAlNQU5JVkFMREUxGjAYBgNVBAUTEVBOT0VFLTMxMTExMTExMTExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgcfk+eY6dvVyDDPpJPkoKpQ08pQx5Jpfjgq+G31lRSsx03y4WYWQhILu5R4isI6DGzQ1MK2dEsW9Dl+S39y7mDDqGlviVpxCtgz14H7NG84ew8vd+sBeaYCvEhKS4+FxRWCmg5VCozr3s2Evi/ao3Wj51ThtecVmAY7PoE27Zckr0GJ/0I+JqEQx19POBr/lNkZN1AxBy8O9gvDzdpCa2Vn9qahY9eZnDGScrP2KsR34UlXa5PjEMVPtSB4btPi9VOQuRoZImGchfUyf1A2uyIPhV5aC+Zgl60B65WxZ+/nEsVN4NoSgBUv+HlwuRxJPelQKeA9tPwKroqO9PGc5/ee2C1HLH7loD+SwahSPMOY2e8CQd6pLmRF1a/H+ZPWZBW+U7Ekm3YeNNJToUkuonAQB/JbwBvHkZXwsH4/kMHyMPiws5G3nr/jyqF2595KKghIgjGHR1WhGljQzdgO5LT4uuOhesGDRYeMUanvClWSb/mt0SdS8njziY7WoYPYFFFgjRvIIK5FgOd8d2W88I5pj2/SjcXb6GMqEqI3HkCBGPDSo57nSJZzJD8KjJs/4jvzZnGwCFZ8+jeyh562B01mkFfwFaoFOYfqRG3g5sGdZUdY9Nk3FZ8dgEwylUMSxmaL0R2/mzNVasFWp482eHwlK2rae3v+QtCHGfOKn+vsCAwEAAaOCAdIwggHOMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMFYGA1UdIARPME0wQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABATAdBgNVHQ4EFgQUNxW1gjoB4+Qh46Rj3SuULubhtUMwgZkGCCsGAQUFBwEDBIGMMIGJMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCH+SY8KKgw5UDlVL99ToRWPpcloyfOM64UTnNgEDXDDI5r1CNNA0OlggzoEZfakNQJamHjIT287LV7nXFsB4Q9VzyI3H1J5mzVIZrMUiE68wf25BDuA3Zwpri+f8Me78f3nowO2cJ2AiMJ83vQFKKy1LFOixWguuxioKlda2Jq7B57ty5cN+jZwLO7Vrv4Tryg9QeOaxnFvHvuZaxMnE55of7cLpfyAH/5DKvlXx4cdmh7kNO4F/o2LT7om4Cf+Sq6tFS3cUn4zcQbFKT5lw+7vfewzG6X0qYnHbe7Ts/zhh7IJpHnPF1p23ND0+jHgbcDVTFjV4pN1PhVthYHOMeDW461okw2OA/jfuQetUlDwqT5yCdjrOTMDkjZCjTMhcVPzw+7hSUUnewKiR0smuyZbKpE/ZGZWUA6K0sieGCpHGKJo99zD3zmEWmOmq++D0TmVvEiXVJs8fuNWl+VmXSStkMeNR4noHAL1PFUebXVS0lPpQZzBKgqhMGAgbwvYajZnOlvXVll6QashxFZmOVNy88O67s+a2p1SmQTtqNrlodszqkKsc28nDbbvBUd4PUD5tmVgPe29Zwnm1TxFuhl0gqvVc+qZme8zq6yd3nCKNrY6qron4Xcc1rxCWS7NcyO5JiF+qXgAuDOkSFJaaEnQh83ZJsNneXD/nyBH8kSiQ=="; + public static final String CERTIFICATE = "MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTYxMjA5MTYyNDU2WhcNMTkxMjA5MTYyNDU2WjCBvzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMS0wKwYDVQQDDCRFTEZSSUlEQSxNQU5JVkFMREUsUE5PRUUtMzExMTExMTExMTExETAPBgNVBAQMCEVMRlJJSURBMRIwEAYDVQQqDAlNQU5JVkFMREUxGjAYBgNVBAUTEVBOT0VFLTMxMTExMTExMTExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgcfk+eY6dvVyDDPpJPkoKpQ08pQx5Jpfjgq+G31lRSsx03y4WYWQhILu5R4isI6DGzQ1MK2dEsW9Dl+S39y7mDDqGlviVpxCtgz14H7NG84ew8vd+sBeaYCvEhKS4+FxRWCmg5VCozr3s2Evi/ao3Wj51ThtecVmAY7PoE27Zckr0GJ/0I+JqEQx19POBr/lNkZN1AxBy8O9gvDzdpCa2Vn9qahY9eZnDGScrP2KsR34UlXa5PjEMVPtSB4btPi9VOQuRoZImGchfUyf1A2uyIPhV5aC+Zgl60B65WxZ+/nEsVN4NoSgBUv+HlwuRxJPelQKeA9tPwKroqO9PGc5/ee2C1HLH7loD+SwahSPMOY2e8CQd6pLmRF1a/H+ZPWZBW+U7Ekm3YeNNJToUkuonAQB/JbwBvHkZXwsH4/kMHyMPiws5G3nr/jyqF2595KKghIgjGHR1WhGljQzdgO5LT4uuOhesGDRYeMUanvClWSb/mt0SdS8njziY7WoYPYFFFgjRvIIK5FgOd8d2W88I5pj2/SjcXb6GMqEqI3HkCBGPDSo57nSJZzJD8KjJs/4jvzZnGwCFZ8+jeyh562B01mkFfwFaoFOYfqRG3g5sGdZUdY9Nk3FZ8dgEwylUMSxmaL0R2/mzNVasFWp482eHwlK2rae3v+QtCHGfOKn+vsCAwEAAaOCAdIwggHOMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMFYGA1UdIARPME0wQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABATAdBgNVHQ4EFgQUNxW1gjoB4+Qh46Rj3SuULubhtUMwgZkGCCsGAQUFBwEDBIGMMIGJMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCH+SY8KKgw5UDlVL99ToRWPpcloyfOM64UTnNgEDXDDI5r1CNNA0OlggzoEZfakNQJamHjIT287LV7nXFsB4Q9VzyI3H1J5mzVIZrMUiE68wf25BDuA3Zwpri+f8Me78f3nowO2cJ2AiMJ83vQFKKy1LFOixWguuxioKlda2Jq7B57ty5cN+jZwLO7Vrv4Tryg9QeOaxnFvHvuZaxMnE55of7cLpfyAH/5DKvlXx4cdmh7kNO4F/o2LT7om4Cf+Sq6tFS3cUn4zcQbFKT5lw+7vfewzG6X0qYnHbe7Ts/zhh7IJpHnPF1p23ND0+jHgbcDVTFjV4pN1PhVthYHOMeDW461okw2OA/jfuQetUlDwqT5yCdjrOTMDkjZCjTMhcVPzw+7hSUUnewKiR0smuyZbKpE/ZGZWUA6K0sieGCpHGKJo99zD3zmEWmOmq++D0TmVvEiXVJs8fuNWl+VmXSStkMeNR4noHAL1PFUebXVS0lPpQZzBKgqhMGAgbwvYajZnOlvXVll6QashxFZmOVNy88O67s+a2p1SmQTtqNrlodszqkKsc28nDbbvBUd4PUD5tmVgPe29Zwnm1TxFuhl0gqvVc+qZme8zq6yd3nCKNrY6qron4Xcc1rxCWS7NcyO5JiF+qXgAuDOkSFJaaEnQh83ZJsNneXD/nyBH8kSiQ=="; public static SessionResult createSessionEndResult() { SessionResult result = createSessionResult("OK"); diff --git a/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java index 31c0df0e..03ea6fdc 100644 --- a/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,27 +27,27 @@ */ -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.test.smartid.integration.SmartIdIntegrationTest; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import org.hamcrest.core.StringContains; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.io.InputStream; -import java.security.KeyStore; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; -; +import java.io.InputStream; +import java.security.KeyStore; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.junit.jupiter.api.Test; + +import ee.sk.FileUtil; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.test.smartid.integration.SmartIdIntegrationTest; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; public class EndpointSslVerificationIntegrationTest { @@ -55,70 +55,29 @@ public class EndpointSslVerificationIntegrationTest { private static final String LIVE_HOST_URL = "https://rp-api.smart-id.com/v1"; private static final String DEMO_RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; private static final String DEMO_RELYING_PARTY_NAME = "DEMO"; - private static final String DEMO_DOCUMENT_NUMBER = "PNOLT-30303039914-MOCK-Q"; - - public static final String LIVE_HOST_SSL_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN\nMjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb\nMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h\ncnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS\nlaHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj\njgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt\n/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO\ndRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan\ngnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38\nyJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN\nRji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX\nMBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW\nMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v\nY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov\nL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3\nBglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu\nY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl\ncnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw\nDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K\ncbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E\noZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd\n0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g\ngw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW\nw+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu\nzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU\n9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1\nfM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2\nFJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg\nlWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t\nYv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu\nulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi\n/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3\ndgE=\n-----END CERTIFICATE-----\n"; - - public static final String DEMO_HOST_SSL_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIIGoDCCBYigAwIBAgIQBOJYR4uzB/mihrGnWl+QIjANBgkqhkiG9w0BAQsFADBP\n" - + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE\n" - + "aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjA5MTYwMDAwMDBa\n" - + "Fw0yMzEwMTcyMzU5NTlaMFUxCzAJBgNVBAYTAkVFMRAwDgYDVQQHEwdUYWxsaW5u\n" - + "MRswGQYDVQQKExJTSyBJRCBTb2x1dGlvbnMgQVMxFzAVBgNVBAMTDnNpZC5kZW1v\n" - + "LnNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoDLLTK+NEKsB\n" - + "POdOEjAK7/A8JTmZXlRkjM1aX0pfH6BCIGn3ZJd9M6iSR+KKQEfT0cj7JWvfMjZT\n" - + "oVHxOPbUaIUTdu22akLDy0kuZN78/RdqHUPq9WTKZsG3r03bi6tGqFb2KfzhZ2Q9\n" - + "zfS8Yn5N0iPeMh48BsreEdumb4F97JSEzjzFdGBb5wED//pHUL2VRoX1hzKV/6D8\n" - + "/sWmbMdGTYcXds/JbOIFU6EgAO2ozJUQmTbR2XRJYawKYAm4CEyY49zzvOldjOUC\n" - + "VjbheCxPJB0OeqYmfxm6QNqEi33Jsof9Y8uRl/DrEGexApd0bQkcGoGyBB08MWyu\n" - + "xjjmjh6TSQIDAQABo4IDcDCCA2wwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iy\n" - + "xZV2ufQwHQYDVR0OBBYEFIrtybLjSa2jrMVWly+c7KCBvpifMBkGA1UdEQQSMBCC\n" - + "DnNpZC5kZW1vLnNrLmVlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF\n" - + "BQcDAQYIKwYBBQUHAwIwgY8GA1UdHwSBhzCBhDBAoD6gPIY6aHR0cDovL2NybDMu\n" - + "ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDBA\n" - + "oD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hB\n" - + "MjU2MjAyMENBMS00LmNybDA+BgNVHSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUF\n" - + "BwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwfwYIKwYBBQUHAQEEczBx\n" - + "MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSQYIKwYBBQUH\n" - + "MAKGPWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNI\n" - + "QTI1NjIwMjBDQTEtMS5jcnQwCQYDVR0TBAIwADCCAYAGCisGAQQB1nkCBAIEggFw\n" - + "BIIBbAFqAHcA6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGDRaWg\n" - + "0AAABAMASDBGAiEA0YjYuhVcbwncKefVPz4d8IrAQQ6ahcw5mOFufHTwbV8CIQCk\n" - + "oYVmHeYe9C9WeHYT4sKozs3ubeNqxPDRjKKaCPhtzQB2ADXPGRu/sWxXvw+tTG1C\n" - + "y7u2JyAmUeo/4SrvqAPDO9ZMAAABg0WloQQAAAQDAEcwRQIhALhRwut2GdVSxBnG\n" - + "KJOvCyaCySEhF7CXkhJRYsaZhBADAiB2X85UxwB5030w+1pX0QxJ4Z3A2sLwrwYR\n" - + "9/+yt4NGLwB3ALc++yTfnE26dfI5xbpY9Gxd/ELPep81xJ4dCYEl7bSZAAABg0Wl\n" - + "oRUAAAQDAEgwRgIhAPFc0KtyRqpNV3muD5aCzgE0RuQxsz6KPYKX4I49hfZeAiEA\n" - + "yuqiqCAtBkt/G7Wq4SA+/4xDyRKwXo5Zu8QuGGx9taYwDQYJKoZIhvcNAQELBQAD\n" - + "ggEBADTzrIM6pAvIClyXTGtyceDKckkGENmFmDvwL6I0Tab/s8uLlREpDhRPQpFQ\n" - + "hsAjaxWrfUv25EdYelBvaiOrCUwI3W3zlLy4gcgagEyTJ71lz7cH0VwFWjTsfXXc\n" - + "osD5sXMfipvkgmX+XgYJjsDY/HDFQyZp7aoTVqAlOfqkfsHi1EGdd6AGKP0yHokU\n" - + "3sUH1X6kDQdSfu1iwRPCn1CGS6xU1VJ6mJDU8SioBQKBAQkCs5UVdjdH+o99xsND\n" - + "8kfVHlchc+SxsI5cYhc4gUjjtX/U3FDZcW1IfZDil9tQf9l6rU/ZXMIPHeQWTPAa\n" - + "nUMrQKgVkBFH6CVchyHXPejDNGA=\n" - + "-----END CERTIFICATE-----"; - - @Rule - public ExpectedException expectedException = ExpectedException.none(); + private static final String DEMO_DOCUMENT_NUMBER = "PNOLT-50609019996-MOCK-Q"; + + private static final String LIVE_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_live_sk_ee.pem"); + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); @Test public void makeRequestToDemoApi_useLiveEnvCertificates_sslHandshakeFails() { - expectedException.expect(ProcessingException.class); - expectedException.expectMessage(StringContains.containsString("unable to find valid certification path to requested target")); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - - client.setHostUrl(DEMO_HOST_URL); - client.setTrustedCertificates(LIVE_HOST_SSL_CERTIFICATE); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); + var processingException = assertThrows(ProcessingException.class, () -> { + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); + client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); + + client.setHostUrl(DEMO_HOST_URL); + client.setTrustedCertificates(LIVE_HOST_SSL_CERTIFICATE); + + client + .getCertificate() + .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) + .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) + .withDocumentNumber(DEMO_DOCUMENT_NUMBER) + .fetch(); + }); + assertThat(processingException.getMessage(), containsString("unable to find valid certification path to requested target")); } @Test @@ -141,54 +100,54 @@ public void makeRequestToDemoApi_useDemoEnvCertificates_sslHandshakeSuccess() { } @Test - public void makeRequestToLiveApi_trustStoreFile() throws Exception { - expectedException.expect(RelyingPartyAccountConfigurationException.class); - - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(LIVE_HOST_URL); - client.setTrustStore(trustStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); + public void makeRequestToLiveApi_trustStoreFile() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(is, "changeit".toCharArray()); + + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); + client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); + client.setHostUrl(LIVE_HOST_URL); + client.setTrustStore(trustStore); + + client + .getCertificate() + .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) + .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) + .withDocumentNumber(DEMO_DOCUMENT_NUMBER) + .fetch(); + }); } @Test - public void makeRequestToLiveApi_trustStoreContext() throws Exception { - expectedException.expect(RelyingPartyAccountConfigurationException.class); - - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - - - SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(trustStore); - trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(LIVE_HOST_URL); - client.setTrustSslContext(trustSslContext); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); + public void makeRequestToLiveApi_trustStoreContext() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(is, "changeit".toCharArray()); + + + SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(trustStore); + trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); + + + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); + client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); + client.setHostUrl(LIVE_HOST_URL); + client.setTrustSslContext(trustSslContext); + + client + .getCertificate() + .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) + .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) + .withDocumentNumber(DEMO_DOCUMENT_NUMBER) + .fetch(); + }); } @Test @@ -283,47 +242,46 @@ public void makeRequestToDemoApi_loadSslCertificatesFromPkcs12TrustStore_sslHand } @Test - public void makeRequestToDemoApi_emptyKeyStore_requestFails() throws Exception { - expectedException.expect(ProcessingException.class); - - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(null, null); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(trustStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); + public void makeRequestToDemoApi_emptyKeyStore_requestFails() { + assertThrows(ProcessingException.class, () -> { + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(null, null); + + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); + client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); + client.setHostUrl(DEMO_HOST_URL); + client.setTrustStore(trustStore); + + client + .getCertificate() + .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) + .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) + .withDocumentNumber(DEMO_DOCUMENT_NUMBER) + .fetch(); + }); } @Test - public void makeRequestToDemoApi_loadWrongSslCertificate_requestFails() throws Exception { - expectedException.expect(ProcessingException.class); - expectedException.expectMessage(StringContains.containsString("unable to find valid certification path to requested target")); - - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/wrong_ssl_cert.jks"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(keyStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); + public void makeRequestToDemoApi_loadWrongSslCertificate_requestFails() { + var processingException = assertThrows(ProcessingException.class, () -> { + InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/wrong_ssl_cert.jks"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(is, "changeit".toCharArray()); + + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); + client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); + client.setHostUrl(DEMO_HOST_URL); + client.setTrustStore(keyStore); + + client + .getCertificate() + .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) + .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) + .withDocumentNumber(DEMO_DOCUMENT_NUMBER) + .fetch(); + }); + assertThat(processingException.getMessage(), containsString("unable to find valid certification path to requested target")); } - } diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java index 531abfca..c60fea3c 100644 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,45 +26,45 @@ * #L% */ -import org.apache.commons.codec.binary.Base64; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.Test; public class SignableDataTest { - public static final byte[] DATA_TO_SIGN = "Hello World!".getBytes(); - public static final String SHA512_HASH_IN_BASE64 = "hhhE1nBOhXP+w02WfiC8/vPUJM9IvgTm3AjyvVjHKXQzcQFerYkcw88cnTS0kmS1EHUbH/nlN5N7xGtdb/TsyA=="; - public static final String SHA384_HASH_IN_BASE64 = "v9dsDrvQBv7lg0EFR8GIewKSvnbVgtlsJC0qeScj4/1v0GH51c/RO4+WE1jmrbpK"; - public static final String SHA256_HASH_IN_BASE64 = "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk="; + private static final byte[] DATA_TO_SIGN = "Hello World!".getBytes(); + private static final String SHA512_HASH_IN_BASE64 = "hhhE1nBOhXP+w02WfiC8/vPUJM9IvgTm3AjyvVjHKXQzcQFerYkcw88cnTS0kmS1EHUbH/nlN5N7xGtdb/TsyA=="; + private static final String SHA384_HASH_IN_BASE64 = "v9dsDrvQBv7lg0EFR8GIewKSvnbVgtlsJC0qeScj4/1v0GH51c/RO4+WE1jmrbpK"; + private static final String SHA256_HASH_IN_BASE64 = "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk="; - @Test - public void signableData_withDefaultHashType_sha512() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - assertEquals("SHA512", signableData.getHashType().getHashTypeName()); - assertEquals(SHA512_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA512_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("4664", signableData.calculateVerificationCode()); - } + @Test + public void signableData_withDefaultHashType_sha512() { + SignableData signableData = new SignableData(DATA_TO_SIGN); + assertEquals("SHA512", signableData.getHashType().getHashTypeName()); + assertEquals(SHA512_HASH_IN_BASE64, signableData.calculateHashInBase64()); + assertArrayEquals(Base64.decodeBase64(SHA512_HASH_IN_BASE64), signableData.calculateHash()); + assertEquals("4664", signableData.calculateVerificationCode()); + } - @Test - public void signableData_with_sha256() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - signableData.setHashType(HashType.SHA256); - assertEquals("SHA256", signableData.getHashType().getHashTypeName()); - assertEquals(SHA256_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA256_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("7712", signableData.calculateVerificationCode()); - } + @Test + public void signableData_with_sha256() { + SignableData signableData = new SignableData(DATA_TO_SIGN); + signableData.setHashType(HashType.SHA256); + assertEquals("SHA256", signableData.getHashType().getHashTypeName()); + assertEquals(SHA256_HASH_IN_BASE64, signableData.calculateHashInBase64()); + assertArrayEquals(Base64.decodeBase64(SHA256_HASH_IN_BASE64), signableData.calculateHash()); + assertEquals("7712", signableData.calculateVerificationCode()); + } - @Test - public void signableData_with_sha384() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - signableData.setHashType(HashType.SHA384); - assertEquals("SHA384", signableData.getHashType().getHashTypeName()); - assertEquals(SHA384_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA384_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("3486", signableData.calculateVerificationCode()); - } + @Test + public void signableData_with_sha384() { + SignableData signableData = new SignableData(DATA_TO_SIGN); + signableData.setHashType(HashType.SHA384); + assertEquals("SHA384", signableData.getHashType().getHashTypeName()); + assertEquals(SHA384_HASH_IN_BASE64, signableData.calculateHashInBase64()); + assertArrayEquals(Base64.decodeBase64(SHA384_HASH_IN_BASE64), signableData.calculateHash()); + assertEquals("3486", signableData.calculateVerificationCode()); + } } diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/SignableHashTest.java index c0e93280..bddca5b0 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/SignableHashTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,24 +26,25 @@ * #L% */ -import org.junit.Assert; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; public class SignableHashTest { - @Test - public void calculateVerificationCodeWithSha256() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - Assert.assertEquals("4240", hashToSign.calculateVerificationCode()); - } + @Test + public void calculateVerificationCodeWithSha256() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + assertEquals("4240", hashToSign.calculateVerificationCode()); + } - @Test - public void calculateVerificationCodeWithSha512() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA512); - hashToSign.setHash(DigestCalculator.calculateDigest("Hello World!".getBytes(), HashType.SHA512)); - Assert.assertEquals("4664", hashToSign.calculateVerificationCode()); - } + @Test + public void calculateVerificationCodeWithSha512() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA512); + hashToSign.setHash(DigestCalculator.calculateDigest("Hello World!".getBytes(), HashType.SHA512)); + assertEquals("4664", hashToSign.calculateVerificationCode()); + } } diff --git a/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java index 0f44e1c6..ad93d37c 100644 --- a/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,511 +26,518 @@ * #L% */ +import static ee.sk.smartid.DummyData.createSessionEndResult; +import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; +import static ee.sk.smartid.DummyData.createUserSelectedWrongVerificationCode; +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.*; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.rest.SessionStatusPoller; import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.*; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.util.Collections; - -import static ee.sk.smartid.DummyData.*; -import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.*; +import ee.sk.smartid.rest.dao.Capability; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionResponse; public class SignatureRequestBuilderTest { - private SmartIdConnectorSpy connector; - private SignatureRequestBuilder builder; - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @Before - public void setUp() { - connector = new SmartIdConnectorSpy(); - connector.signatureSessionResponseToRespond = createDummySignatureSessionResponse(); - connector.sessionStatusToRespond = createDummySessionStatusResponse(); - builder = new SignatureRequestBuilder(connector, new SessionStatusPoller(connector)); - } - - @Test - public void sign_withHashToSign() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Sign hash?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withDataToSign() { - SignableData dataToSign = new SignableData("Say 'hello' to my little friend!".getBytes()); - dataToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableData(dataToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Do you want to say hello?"))) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withoutCertificateLevel() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .sign(); - - assertCorrectSignatureRequestMade(null); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withShareMdClientIpAddressTrue() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .withShareMdClientIpAddress(true) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.signatureSessionRequestUsed.getRequestProperties()); - - assertTrue("requestProperties.shareMdClientIpAddress must be true", - connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withShareMdClientIpAddressFalse() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .withShareMdClientIpAddress(false) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - - assertNotNull("getRequestProperties must be set withShareMdClientIpAddress", - connector.signatureSessionRequestUsed.getRequestProperties()); - - assertFalse("requestProperties.shareMdClientIpAddress must be false", - connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress()); - - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void signWithoutDocumentNumber_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either documentNumber or semanticsIdentifier must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .sign(); - } - - @Test - public void sign_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Exactly one of documentNumber or semanticsIdentifier must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .sign(); - } - - @Test - public void sign_withoutDataToSign_withoutHash_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .sign(); - } - - @Test - public void signWithSignableHash_withoutHashType_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - } - - @Test - public void sign_withHash_withoutHashType_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Either dataToSign or hash with hashType must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - } - - @Test - public void sign_withoutRelyingPartyUuid_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Parameter relyingPartyUUID must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - } - - @Test - public void sign_withoutRelyingPartyName_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Parameter relyingPartyName must be set"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - } - - @Test - public void sign_withTooLongNonce_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .sign(); - } - - - @Test - public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText60 must not be longer than 60 characters"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) - ) - .sign(); - } - - @Test - public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText60 must not be longer than 60 characters"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) - ) - .sign(); - } - - @Test - public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText200 must not be longer than 200 characters"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .sign(); - } - - @Test - public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { - expectedException.expect(SmartIdClientException.class); - expectedException.expectMessage("displayText200 must not be longer than 200 characters"); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .sign(); - } - - - - @Test - public void sign_userRefused_shouldThrowException() { - expectedException.expect(UserRefusedException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - makeSigningRequest(); - } - - - @Test - public void sign_userRefusedCertChoice_shouldThrowException() { - expectedException.expect(UserRefusedCertChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); - makeSigningRequest(); - } - - @Test - public void sign_userRefusedDisplayTextAndPin_shouldThrowException() { - expectedException.expect(UserRefusedDisplayTextAndPinException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); - makeSigningRequest(); - } - - @Test - public void sign_userRefusedVerificationChoice_shouldThrowException() { - expectedException.expect(UserRefusedVerificationChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); - makeSigningRequest(); - } - - @Test - public void sign_userRefusedConfirmationMessage_shouldThrowException() { - expectedException.expect(UserRefusedConfirmationMessageException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); - makeSigningRequest(); - } - - @Test - public void sign_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { - expectedException.expect(UserRefusedConfirmationMessageWithVerificationChoiceException.class); - - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - makeSigningRequest(); - } - - @Test - public void sign_userSelectedWrongVerificationCode_shouldThrowException() { - expectedException.expect(UserSelectedWrongVerificationCodeException.class); - - connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); - makeSigningRequest(); - } - - @Test - public void sign_signatureMissingInResponse_shouldThrowException() { - expectedException.expect(UnprocessableSmartIdResponseException.class); - expectedException.expectMessage("Signature was not present in the response"); - - connector.sessionStatusToRespond.setSignature(null); - makeSigningRequest(); - } - - private void assertCorrectSignatureRequestMade(String expectedCertificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.signatureSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.signatureSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.signatureSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA256", connector.signatureSessionRequestUsed.getHashType()); - assertEquals("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ=", connector.signatureSessionRequestUsed.getHash()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertSignatureCorrect(SmartIdSignature signature) { - assertNotNull(signature); - assertEquals("luvjsi1+1iLN9yfDFEh/BE8h", signature.getValueInBase64()); - assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); - assertEquals("PNOEE-31111111111", signature.getDocumentNumber()); - assertThat(signature.getInteractionFlowUsed(), is("verificationCodeChoice")); - } - - private SignatureSessionResponse createDummySignatureSessionResponse() { - SignatureSessionResponse response = new SignatureSessionResponse(); - response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return response; - } - - private SessionStatus createDummySessionStatusResponse() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setResult(createSessionEndResult()); - SessionSignature signature = new SessionSignature(); - signature.setValue("luvjsi1+1iLN9yfDFEh/BE8h"); - signature.setAlgorithm("sha256WithRSAEncryption"); - status.setSignature(signature); - status.setInteractionFlowUsed("verificationCodeChoice"); - return status; - } - - private void makeSigningRequest() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Transfer amount X to Y?"))) - .sign(); - } + private SmartIdConnectorSpy connector; + private SignatureRequestBuilder builder; + + @BeforeEach + public void setUp() { + connector = new SmartIdConnectorSpy(); + connector.signatureSessionResponseToRespond = createDummySignatureSessionResponse(); + connector.sessionStatusToRespond = createDummySessionStatusResponse(); + builder = new SignatureRequestBuilder(connector, new SessionStatusPoller(connector)); + } + + @Test + public void sign_withHashToSign() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + + SmartIdSignature signature = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withCapabilities(Capability.ADVANCED) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Sign hash?"), + Interaction.verificationCodeChoice("Sign hash?"))) + .sign(); + + assertCorrectSignatureRequestMade("QUALIFIED"); + assertCorrectSessionRequestMade(); + assertSignatureCorrect(signature); + } + + @Test + public void sign_withDataToSign() { + SignableData dataToSign = new SignableData("Say 'hello' to my little friend!".getBytes()); + dataToSign.setHashType(HashType.SHA256); + + SmartIdSignature signature = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableData(dataToSign) + .withDocumentNumber("PNOEE-31111111111") + .withCapabilities("QUALIFIED") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Do you want to say hello?"))) + .sign(); + + assertCorrectSignatureRequestMade("QUALIFIED"); + assertCorrectSessionRequestMade(); + assertSignatureCorrect(signature); + } + + @Test + public void sign_withoutCertificateLevel() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + hashToSign.setHashType(HashType.SHA256); + + SmartIdSignature signature = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), + Interaction.verificationCodeChoice("Sign hash?"))) + .sign(); + + assertCorrectSignatureRequestMade(null); + assertCorrectSessionRequestMade(); + assertSignatureCorrect(signature); + } + + @Test + public void sign_withShareMdClientIpAddressTrue() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + hashToSign.setHashType(HashType.SHA256); + + SmartIdSignature signature = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), + Interaction.verificationCodeChoice("Sign hash?"))) + .withShareMdClientIpAddress(true) + .sign(); + + assertCorrectSignatureRequestMade("QUALIFIED"); + + assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + + assertTrue(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), + "requestProperties.shareMdClientIpAddress must be true"); + + assertCorrectSessionRequestMade(); + assertSignatureCorrect(signature); + } + + @Test + public void sign_withShareMdClientIpAddressFalse() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + hashToSign.setHashType(HashType.SHA256); + + SmartIdSignature signature = builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), + Interaction.verificationCodeChoice("Sign hash?"))) + .withShareMdClientIpAddress(false) + .sign(); + + assertCorrectSignatureRequestMade("QUALIFIED"); + + assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + + assertFalse(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), + "requestProperties.shareMdClientIpAddress must be false"); + + assertCorrectSessionRequestMade(); + assertSignatureCorrect(signature); + } + + @Test + public void signWithoutDocumentNumber_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .sign(); + }); + assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withSemanticsIdentifierAsString("IDCCZ-1234567890") + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .sign(); + }); + assertEquals("Exactly one of documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withoutDataToSign_withoutHash_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, + () -> builder.withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .sign()); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void signWithSignableHash_withoutHashType_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .sign(); + }); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withHash_withoutHashType_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .sign(); + }); + assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withoutRelyingPartyUuid_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .sign(); + }); + assertEquals("Parameter relyingPartyUUID must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withoutRelyingPartyName_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .sign(); + }); + assertEquals("Parameter relyingPartyName must be set", smartIdClientException.getMessage()); + } + + @Test + public void sign_withTooLongNonce_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") + .sign(); + }); + assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); + } + + + @Test + public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + hashToSign.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) + ) + .sign(); + }); + assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + hashToSign.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) + ) + .sign(); + }); + assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + hashToSign.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) + ) + .sign(); + }); + assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); + } + + @Test + public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); + hashToSign.setHashType(HashType.SHA512); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) + ) + .sign(); + }); + assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); + } + + + @Test + public void sign_userRefused_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + makeSigningRequest(); + }); + } + + + @Test + public void sign_userRefusedCertChoice_shouldThrowException() { + assertThrows(UserRefusedCertChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); + makeSigningRequest(); + }); + } + + @Test + public void sign_userRefusedDisplayTextAndPin_shouldThrowException() { + assertThrows(UserRefusedDisplayTextAndPinException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); + makeSigningRequest(); + }); + } + + @Test + public void sign_userRefusedVerificationChoice_shouldThrowException() { + assertThrows(UserRefusedVerificationChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); + makeSigningRequest(); + }); + } + + @Test + public void sign_userRefusedConfirmationMessage_shouldThrowException() { + assertThrows(UserRefusedConfirmationMessageException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); + makeSigningRequest(); + }); + } + + @Test + public void sign_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { + assertThrows(UserRefusedConfirmationMessageWithVerificationChoiceException.class, () -> { + connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); + makeSigningRequest(); + }); + } + + @Test + public void sign_userSelectedWrongVerificationCode_shouldThrowException() { + assertThrows(UserSelectedWrongVerificationCodeException.class, () -> { + connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); + makeSigningRequest(); + }); + } + + @Test + public void sign_signatureMissingInResponse_shouldThrowException() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + connector.sessionStatusToRespond.setSignature(null); + makeSigningRequest(); + }); + assertEquals("Signature was not present in the response", unprocessableSmartIdResponseException.getMessage()); + } + + private void assertCorrectSignatureRequestMade(String expectedCertificateLevel) { + assertEquals("PNOEE-31111111111", connector.documentNumberUsed); + assertEquals("relying-party-uuid", connector.signatureSessionRequestUsed.getRelyingPartyUUID()); + assertEquals("relying-party-name", connector.signatureSessionRequestUsed.getRelyingPartyName()); + assertEquals(expectedCertificateLevel, connector.signatureSessionRequestUsed.getCertificateLevel()); + assertEquals("SHA256", connector.signatureSessionRequestUsed.getHashType()); + assertEquals("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ=", connector.signatureSessionRequestUsed.getHash()); + } + + private void assertCorrectSessionRequestMade() { + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); + } + + private void assertSignatureCorrect(SmartIdSignature signature) { + assertNotNull(signature); + assertEquals("luvjsi1+1iLN9yfDFEh/BE8h", signature.getValueInBase64()); + assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); + assertEquals("PNOEE-31111111111", signature.getDocumentNumber()); + assertThat(signature.getInteractionFlowUsed(), is("verificationCodeChoice")); + } + + private SignatureSessionResponse createDummySignatureSessionResponse() { + SignatureSessionResponse response = new SignatureSessionResponse(); + response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + return response; + } + + private SessionStatus createDummySessionStatusResponse() { + SessionStatus status = new SessionStatus(); + status.setState("COMPLETE"); + status.setResult(createSessionEndResult()); + SessionSignature signature = new SessionSignature(); + signature.setValue("luvjsi1+1iLN9yfDFEh/BE8h"); + signature.setAlgorithm("sha256WithRSAEncryption"); + status.setSignature(signature); + status.setInteractionFlowUsed("verificationCodeChoice"); + return status; + } + + private void makeSigningRequest() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); + hashToSign.setHashType(HashType.SHA256); + + builder + .withRelyingPartyUUID("relying-party-uuid") + .withRelyingPartyName("relying-party-name") + .withCertificateLevel("QUALIFIED") + .withSignableHash(hashToSign) + .withDocumentNumber("PNOEE-31111111111") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Transfer amount X to Y?"))) + .sign(); + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java index f5705810..7488221a 100644 --- a/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,41 +26,47 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.apache.commons.codec.binary.Base64; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.security.cert.CertificateEncodingException; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + public class SmartIdAuthenticationResponseTest { - @Test - public void getSignatureValueInBase64() { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ=="); - assertEquals("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ==", AuthenticationResponse.getSignatureValueInBase64()); - } - @Test - public void getSignatureValueInBytes() { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("VGVyZSBhbGxraXJpIQ=="); - assertArrayEquals("Tere allkiri!".getBytes(), AuthenticationResponse.getSignatureValue()); - } + @Test + public void getSignatureValueInBase64() { + SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); + AuthenticationResponse.setSignatureValueInBase64("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ=="); + assertEquals("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ==", AuthenticationResponse.getSignatureValueInBase64()); + } + + @Test + public void getSignatureValueInBytes() { + SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); + AuthenticationResponse.setSignatureValueInBase64("VGVyZSBhbGxraXJpIQ=="); + assertArrayEquals("Tere allkiri!".getBytes(), AuthenticationResponse.getSignatureValue()); + } - @Test(expected = UnprocessableSmartIdResponseException.class) - public void incorrectBase64StringShouldThrowException() { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("!IsNotValidBase64Character"); - AuthenticationResponse.getSignatureValue(); - } + @Test + public void incorrectBase64StringShouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); + AuthenticationResponse.setSignatureValueInBase64("!IsNotValidBase64Character"); + AuthenticationResponse.getSignatureValue(); + }); + } - @Test - public void getCertificate() throws CertificateEncodingException { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setCertificate(CertificateParser.parseX509Certificate(DummyData.CERTIFICATE)); - assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(AuthenticationResponse.getCertificate().getEncoded())); - } + @Test + public void getCertificate() throws CertificateEncodingException { + SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); + AuthenticationResponse.setCertificate(CertificateParser.parseX509Certificate(DummyData.CERTIFICATE)); + assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(AuthenticationResponse.getCertificate().getEncoded())); + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 425ae613..2c33fa75 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,12 +26,56 @@ * #L% */ -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubSessionStatusWithState; +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.oneOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.*; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.rest.SmartIdConnector; @@ -41,1086 +85,1108 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier.CountryCode; import ee.sk.smartid.rest.dao.SemanticsIdentifier.IdentityType; import ee.sk.smartid.rest.dao.SessionStatus; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; -import static ee.sk.smartid.SmartIdRestServiceStubs.*; -import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +@WireMockTest(httpPort = 18089) public class SmartIdClientTest { - @Rule - public WireMockRule wireMockRule = new WireMockRule(18089); - - private SmartIdClient client; - - @Before - public void setUp() { - client = new SmartIdClient(); - client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - client.setRelyingPartyName("BANK123"); - client.setHostUrl("http://localhost:18089"); - client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN\nMjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb\nMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h\ncnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS\nlaHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj\njgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt\n/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO\ndRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan\ngnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38\nyJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN\nRji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX\nMBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW\nMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v\nY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov\nL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3\nBglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu\nY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl\ncnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw\nDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K\ncbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E\noZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd\n0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g\ngw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW\nw+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu\nzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU\n9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1\nfM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2\nFJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg\nlWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t\nYv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu\nulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi\n/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3\ndgE=\n-----END CERTIFICATE-----\n"); - - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithSha512.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); - - stubRequestWithResponse("/signature/etsi/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/PASEE-987654321012", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/IDCEE-AA3456789", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json"); - - stubRequestWithResponse("/authentication/document/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PASEE-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/IDCEE-AA3456789", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - - stubRequestWithResponse("/certificatechoice/etsi/PASEE-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/IDCEE-AA3456789", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json"); - } - - @Test - public void testSetup() { - assertThat(client.getRelyingPartyUUID(), is("de305d54-75b4-431b-adb2-eb6b9e546014")); - assertThat(client.getRelyingPartyName(), is("BANK123")); - } - - @Test - public void getCertificateAndSign_fullExample() { - // Provide data bytes to be signed (Default hash type is SHA-512) - SignableData dataToSign = new SignableData("Hello World!".getBytes()); - - // Calculate verification code - assertEquals("4664", dataToSign.calculateVerificationCode()); - - // Get certificate and document number - SmartIdCertificate certificateResponse = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - X509Certificate x509Certificate = certificateResponse.getCertificate(); - String documentNumber = certificateResponse.getDocumentNumber(); - - // Sign the data using the document number - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber(documentNumber) - .withSignableData(dataToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?"))) - .sign(); - - byte[] signatureValue = signature.getValue(); - String algorithmName = signature.getAlgorithmName(); // Returns "sha512WithRSAEncryption" - - String interactionFlowUsed = signature.getInteractionFlowUsed(); - - assertThat(interactionFlowUsed, isOneOf("displayTextAndPIN", "confirmationMessage")); - assertValidSignatureCreated(signature); - } - - @Test - public void getCertificateAndSign_withExistingHash() { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - String documentNumber = certificateResponse.getDocumentNumber(); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber(documentNumber) - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - public void getCertificateUsingSemanticsIdentifier() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - SmartIdCertificate certificate = client - .getCertificate() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - public void getCertificateUsingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - public void getCertificateWithNonce() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); - - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111-NONCE") - .withCertificateLevel("ADVANCED") - .withNonce("zstOt2umlc") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - public void getCertificateWithManualSessionStatusRequesting() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - - CertificateRequestBuilder builder = client.getCertificate(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); - - assertCertificateResponseValid(certificate); - verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86"))); - } - - @Test(expected = SmartIdClientException.class) - public void noTrustStoreOrTrustedCertificates_shouldThrowException() { - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - client.setRelyingPartyName("BANK123"); - client.setHostUrl("http://localhost:18089"); - - CertificateRequestBuilder builder = client.getCertificate(); - builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - client.getSmartIdConnector(); - } - - @Test - public void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - CertificateRequestBuilder builder = client.getCertificate(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); - - assertCertificateResponseValid(certificate); - verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86?timeoutMs=5000"))); - } - - @Test - public void sign_withDocumentNumber() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - public void sign_withSemanticsIdentifier() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789"); - - SmartIdSignature signature = client - .createSignature() - .withSemanticsIdentifier(semanticsIdentifier) - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - public void signWithNonce() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withNonce("zstOt2umlc") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - public void signWithManualSessionStatusRequesting() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SignatureRequestBuilder builder = client.createSignature(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .initiateSigning(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); - - assertValidSignatureCreated(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00"))); - } - - @Test - public void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - SignatureRequestBuilder builder = client.createSignature(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .initiateSigning(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); - - assertValidSignatureCreated(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=5000"))); - - } - - @Test(expected = UserAccountNotFoundException.class) - public void getCertificate_whenUserAccountNotFound_shouldThrowException() { - stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); - makeGetCertificateRequest(); - } - - @Test(expected = UserAccountNotFoundException.class) - public void sign_whenUserAccountNotFound_shouldThrowException() { - stubNotFoundResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = UserRefusedException.class) - public void getCertificate_whenUserCancels_shouldThrowException() { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUserRefusedGeneral.json"); - makeGetCertificateRequest(); - } - - @Test(expected = UserRefusedException.class) - public void sign_whenUserCancels_shouldThrowException() { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenUserRefusedGeneral.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = SessionTimeoutException.class) - public void sign_whenTimeout_shouldThrowException() { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenTimeout.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = RequiredInteractionNotSupportedByAppException.class) - public void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); - makeAuthenticationRequest(); - } - - @Test(expected = RequiredInteractionNotSupportedByAppException.class) - public void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = DocumentUnusableException.class) - public void getCertificate_whenDocumentUnusable_shouldThrowException() { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenDocumentUnusable.json"); - makeGetCertificateRequest(); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void getCertificate_whenUnknownErrorCode_shouldThrowException() { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUnknownErrorCode.json"); - makeGetCertificateRequest(); - } - - @Test(expected = DocumentUnusableException.class) - public void sign_whenDocumentUnusable_shouldThrowException() { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenDocumentUnusable.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void getCertificate_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); - makeGetCertificateRequest(); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void sign_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); - makeCreateSignatureRequest(); - } - - @Test(expected = NoSuitableAccountOfRequestedTypeFoundException.class) - public void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 471); - makeGetCertificateRequest(); - } - - @Test(expected = PersonShouldViewSmartIdPortalException.class) - public void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 472); - makeGetCertificateRequest(); - } - - - - @Test(expected = SmartIdClientException.class) - public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 480); - makeCreateSignatureRequest(); - } - - @Test(expected = ServerMaintenanceException.class) - public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 580); - makeGetCertificateRequest(); - } - - @Test(expected = ServerMaintenanceException.class) - public void sign_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 580); - makeCreateSignatureRequest(); - } - - @Test - public void setPollingSleepTimeoutForSignatureCreation() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureSigningDuration(); - assertTrue("Duration is " + duration, duration > 2000L); - assertTrue("Duration is " + duration, duration < 3000L); - } - - @Test - public void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); - SmartIdSignature signature = createSignature(); - - assertThat(signature.getDeviceIpAddress(), is(nullValue())); - } - - @Test - public void createSignatureAndGetDeviceIpAddress() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); - SmartIdSignature signature = createSignature(); - - assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); - assertThat(signature.getDeviceIpAddress(), is("62.65.42.46")); - } - - @Test - public void setPollingSleepTimeoutForCertificateChoice() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureCertificateChoiceDuration(); - assertTrue("Duration is " + duration, duration > 2000L); - assertTrue("Duration is " + duration, duration < 3000L); - } - - @Test - public void setSessionStatusResponseSocketTimeout() { - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SmartIdSignature signature = createSignature(); - assertNotNull(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=10000"))); - } - - @Test - public void authenticateUsingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - public void authenticate_usingSemanticsIdentifier() { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withSemanticsIdentifierAsString("PNOEE-31111111111") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - public void authenticateWithNonce() { - stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); - - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOEE-31111111111-WITH-NONCE") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withNonce("g9rp4kjca3") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - public void authenticateWithManualSessionStatusRequesting() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - AuthenticationRequestBuilder builder = client.createAuthentication(); - String sessionId = builder - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .initiateAuthentication(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); - - assertAuthenticationResponseValid(authenticationResponse); - verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb"))); - } - - @Test - public void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - AuthenticationRequestBuilder builder = client.createAuthentication(); - String sessionId = builder - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .initiateAuthentication(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); - - assertAuthenticationResponseValid(authenticationResponse); - verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb?timeoutMs=5000"))); - } - - @Test(expected = UserAccountNotFoundException.class) - public void authenticate_whenUserAccountNotFound_shouldThrowException() { - stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); - makeAuthenticationRequest(); - } - - @Test(expected = UserRefusedException.class) - public void authenticate_whenUserCancels_shouldThrowException() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenUserRefusedGeneral.json"); - makeAuthenticationRequest(); - } - - @Test(expected = SessionTimeoutException.class) - public void authenticate_whenTimeout_shouldThrowException() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenTimeout.json"); - makeAuthenticationRequest(); - } - - @Test(expected = DocumentUnusableException.class) - public void authenticate_whenDocumentUnusable_shouldThrowException() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenDocumentUnusable.json"); - makeAuthenticationRequest(); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void authenticate_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); - makeAuthenticationRequest(); - } - - @Test(expected = SmartIdClientException.class) - public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 480); - makeAuthenticationRequest(); - } - - @Test(expected = ServerMaintenanceException.class) - public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 580); - makeAuthenticationRequest(); - } - - @Test - public void setPollingSleepTimeoutForAuthentication() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureAuthenticationDuration(); - assertTrue("Duration is " + duration, duration > 2000L); - assertTrue("Duration is " + duration, duration < 3000L); - } - - - @Test - public void getDeviceIpAddress_ipAddressNotPresent() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); - - SmartIdAuthenticationResponse authentication = createAuthentication(); - assertThat(authentication.getDeviceIpAddress(), is(nullValue())); - } - - @Test - public void getDeviceIpAddress_ipAddressReturned() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); - - SmartIdAuthenticationResponse authentication = createAuthentication(); - assertThat(authentication.getDeviceIpAddress(), is("62.65.42.45")); - } - - @Test - public void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - - String headerName = "custom-header"; - String headerValue = "Hi!"; - - Map headersToAdd = new HashMap<>(); - headersToAdd.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headersToAdd); - client.setNetworkConnectionConfig(clientConfig); - makeAuthenticationRequest(); - - verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-32222222222-Z1B2-Q")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() { - String headerName = "custom-header"; - String headerValue = "Hello?!"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); - client.setNetworkConnectionConfig(clientConfig); - makeCreateSignatureRequest(); - - verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-31111111111")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCustomHeader() { - String headerName = "custom-header"; - String headerValue = "Man, come on.."; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); - client.setNetworkConnectionConfig(clientConfig); - makeGetCertificateRequest(); - - verify(postRequestedFor(urlEqualTo("/certificatechoice/etsi/PNOEE-31111111111")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void verifySmartIdConnector_whenConnectorIsNotProvided() { - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - assertTrue(smartIdConnector instanceof SmartIdRestConnector); - } - - @Test - public void verifySmartIdConnector_whenConnectorIsProvided() { - final String mock = "MOCK"; - SessionStatus status = mock(SessionStatus.class); - when(status.getState()).thenReturn(mock); - SmartIdConnector connector = mock(SmartIdConnector.class); - when(connector.getSessionStatus(null)).thenReturn(status); - client.setSmartIdConnector(connector); - assertEquals(mock, client.getSmartIdConnector().getSessionStatus(null).getState()); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_noIdentifierGiven() { - - client - .getCertificate() - .withCertificateLevel("ADVANCED") - .fetch(); - - } - - @Test - public void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - public void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - public void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - public void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - public void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - public void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - public void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - SignableHash signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - @Test - public void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - SignableHash signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - @Test - public void getSignatureByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - SignableHash signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - private long measureSigningDuration() { - long startTime = System.currentTimeMillis(); - SmartIdSignature signature = createSignature(); - long endTime = System.currentTimeMillis(); - assertNotNull(signature); - return endTime - startTime; - } - - private SmartIdSignature createSignature() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - return client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - } - - private long measureAuthenticationDuration() { - long startTime = System.currentTimeMillis(); - SmartIdAuthenticationResponse AuthenticationResponse = createAuthentication(); - long endTime = System.currentTimeMillis(); - assertNotNull(AuthenticationResponse); - return endTime - startTime; - } - - private SmartIdAuthenticationResponse createAuthentication() { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - return client - .createAuthentication() - .withDocumentNumber("PNOEE-31111111111") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - } - - private long measureCertificateChoiceDuration() { - long startTime = System.currentTimeMillis(); - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("ADVANCED") - .fetch(); - long endTime = System.currentTimeMillis(); - assertNotNull(certificate); - return endTime - startTime; - } - - private void makeGetCertificateRequest() { - client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - } - - private void makeCreateSignatureRequest() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - } - - private void makeAuthenticationRequest() { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - client - .createAuthentication() - .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - } - - private ClientConfig getClientConfigWithCustomRequestHeaders(Map headers) { - ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); - clientConfig.register(new ClientRequestHeaderFilter(headers)); - return clientConfig; - } - - private void assertCertificateResponseValid(SmartIdCertificate certificate) { - assertNotNull(certificate); - assertNotNull(certificate.getCertificate()); - X509Certificate cert = certificate.getCertificate(); - assertThat(cert.getSubjectDN().getName(), containsString("SERIALNUMBER=PNOEE-31111111111")); - assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); - assertEquals("QUALIFIED", certificate.getCertificateLevel()); - } - - private void assertValidSignatureCreated(SmartIdSignature signature) { - assertNotNull(signature); - assertThat(signature.getValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); - assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); - assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); - } - - private void assertAuthenticationResponseValid(SmartIdAuthenticationResponse authenticationResponse) { - assertNotNull(authenticationResponse); - assertEquals("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", authenticationResponse.getSignedHashInBase64()); - assertEquals("OK", authenticationResponse.getEndResult()); - assertNotNull(authenticationResponse.getCertificate()); - assertThat(authenticationResponse.getSignatureValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); - assertEquals("sha256WithRSAEncryption", authenticationResponse.getAlgorithmName()); - assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); - } - + private SmartIdClient client; + + @BeforeEach + public void setUp() { + client = new SmartIdClient(); + client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + client.setRelyingPartyName("BANK123"); + client.setHostUrl("http://localhost:18089"); + client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN\nMjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb\nMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h\ncnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS\nlaHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj\njgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt\n/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO\ndRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan\ngnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38\nyJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN\nRji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX\nMBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW\nMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v\nY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov\nL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3\nBglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu\nY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl\ncnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw\nDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K\ncbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E\noZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd\n0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g\ngw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW\nw+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu\nzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU\n9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1\nfM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2\nFJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg\nlWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t\nYv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu\nulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi\n/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3\ndgE=\n-----END CERTIFICATE-----\n"); + + stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithSha512.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); + + stubRequestWithResponse("/signature/etsi/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/etsi/PASEE-987654321012", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/etsi/IDCEE-AA3456789", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json"); + + stubRequestWithResponse("/authentication/document/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PASEE-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/IDCEE-AA3456789", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + + stubRequestWithResponse("/certificatechoice/etsi/PASEE-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/IDCEE-AA3456789", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json"); + } + + @Test + public void getCertificateAndSign_fullExample() { + // Provide data bytes to be signed (Default hash type is SHA-512) + SignableData dataToSign = new SignableData("Hello World!".getBytes()); + + // Calculate verification code + assertEquals("4664", dataToSign.calculateVerificationCode()); + + // Get certificate and document number + SmartIdCertificate certificateResponse = client + .getCertificate() + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) + .withCertificateLevel("ADVANCED") + .fetch(); + + X509Certificate x509Certificate = certificateResponse.getCertificate(); + String documentNumber = certificateResponse.getDocumentNumber(); + + // Sign the data using the document number + SmartIdSignature signature = client + .createSignature() + .withDocumentNumber(documentNumber) + .withSignableData(dataToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?"))) + .sign(); + + byte[] signatureValue = signature.getValue(); + String algorithmName = signature.getAlgorithmName(); // Returns "sha512WithRSAEncryption" + + String interactionFlowUsed = signature.getInteractionFlowUsed(); + + assertThat(interactionFlowUsed, is(oneOf("displayTextAndPIN", "confirmationMessage"))); + assertValidSignatureCreated(signature); + } + + @Test + public void getCertificateAndSign_withExistingHash() { + SmartIdCertificate certificateResponse = client + .getCertificate() + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) + .withCertificateLevel("ADVANCED") + .fetch(); + + String documentNumber = certificateResponse.getDocumentNumber(); + + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + SmartIdSignature signature = client + .createSignature() + .withDocumentNumber(documentNumber) + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signature); + } + + @Test + public void getCertificateUsingSemanticsIdentifier() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + SmartIdCertificate certificate = client + .getCertificate() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel("ADVANCED") + .fetch(); + + assertCertificateResponseValid(certificate); + } + + @Test + public void getCertificateUsingDocumentNumber() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + + SmartIdCertificate certificate = client + .getCertificate() + .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") + .withCertificateLevel("ADVANCED") + .fetch(); + + assertCertificateResponseValid(certificate); + } + + @Test + public void getCertificateWithNonce() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + + SmartIdCertificate certificate = client + .getCertificate() + .withDocumentNumber("PNOEE-31111111111-NONCE") + .withCertificateLevel("ADVANCED") + .withNonce("zstOt2umlc") + .fetch(); + + assertCertificateResponseValid(certificate); + } + + @Test + public void getCertificateWithManualSessionStatusRequesting() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + + CertificateRequestBuilder builder = client.getCertificate(); + String sessionId = builder + .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") + .withCertificateLevel("ADVANCED") + .initiateCertificateChoice(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); + + assertCertificateResponseValid(certificate); + verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86"))); + } + + @Test + public void noTrustStoreOrTrustedCertificates_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + client.setRelyingPartyName("BANK123"); + client.setHostUrl("http://localhost:18089"); + + CertificateRequestBuilder builder = client.getCertificate(); + builder + .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") + .withCertificateLevel("ADVANCED") + .initiateCertificateChoice(); + + client.getSmartIdConnector(); + }); + } + + @Test + public void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + + client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); + CertificateRequestBuilder builder = client.getCertificate(); + String sessionId = builder + .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") + .withCertificateLevel("ADVANCED") + .initiateCertificateChoice(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); + + assertCertificateResponseValid(certificate); + verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86?timeoutMs=5000"))); + } + + @Test + public void sign_withDocumentNumber() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + assertEquals("1796", hashToSign.calculateVerificationCode()); + + SmartIdSignature signature = client + .createSignature() + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signature); + } + + @Test + public void sign_withSemanticsIdentifier() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + assertEquals("1796", hashToSign.calculateVerificationCode()); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789"); + + SmartIdSignature signature = client + .createSignature() + .withSemanticsIdentifier(semanticsIdentifier) + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signature); + } + + @Test + public void signWithNonce() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + assertEquals("1796", hashToSign.calculateVerificationCode()); + + SmartIdSignature signature = client + .createSignature() + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withNonce("zstOt2umlc") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signature); + } + + @Test + public void signWithManualSessionStatusRequesting() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + assertEquals("1796", hashToSign.calculateVerificationCode()); + + SignatureRequestBuilder builder = client.createSignature(); + String sessionId = builder + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .initiateSigning(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); + + assertValidSignatureCreated(signature); + verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00"))); + } + + @Test + public void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + assertEquals("1796", hashToSign.calculateVerificationCode()); + + client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); + SignatureRequestBuilder builder = client.createSignature(); + String sessionId = builder + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .initiateSigning(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); + + assertValidSignatureCreated(signature); + verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=5000"))); + + } + + @Test + public void getCertificate_whenUserAccountNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenUserAccountNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); + makeCreateSignatureRequest(); + }); + } + + @Test + public void getCertificate_whenUserCancels_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUserRefusedGeneral.json"); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenUserCancels_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenUserRefusedGeneral.json"); + makeCreateSignatureRequest(); + }); + } + + @Test + public void sign_whenTimeout_shouldThrowException() { + assertThrows(SessionTimeoutException.class, () -> { + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenTimeout.json"); + makeCreateSignatureRequest(); + }); + } + + @Test + public void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { + assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { + assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void getCertificate_whenDocumentUnusable_shouldThrowException() { + assertThrows(DocumentUnusableException.class, () -> { + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenDocumentUnusable.json"); + makeGetCertificateRequest(); + }); + } + + @Test + public void getCertificate_whenUnknownErrorCode_shouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUnknownErrorCode.json"); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenDocumentUnusable_shouldThrowException() { + assertThrows(DocumentUnusableException.class, () -> { + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenDocumentUnusable.json"); + makeCreateSignatureRequest(); + }); + } + + @Test + public void getCertificate_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); + makeCreateSignatureRequest(); + }); + } + + @Test + public void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 471); + makeGetCertificateRequest(); + }); + } + + @Test + public void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 472); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 480); + makeCreateSignatureRequest(); + }); + } + + @Test + public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 580); + makeGetCertificateRequest(); + }); + } + + @Test + public void sign_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 580); + makeCreateSignatureRequest(); + }); + } + + @Test + public void setPollingSleepTimeoutForSignatureCreation() { + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); + client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); + long duration = measureSigningDuration(); + assertTrue(duration > 2000L, "Duration is " + duration); + assertTrue(duration < 3000L, "Duration is " + duration); + } + + @Test + public void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); + SmartIdSignature signature = createSignature(); + + assertThat(signature.getDeviceIpAddress(), is(nullValue())); + } + + @Test + public void createSignatureAndGetDeviceIpAddress() { + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); + SmartIdSignature signature = createSignature(); + + assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); + assertThat(signature.getDeviceIpAddress(), is("62.65.42.46")); + } + + @Test + public void setPollingSleepTimeoutForCertificateChoice() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + + stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json", "COMPLETE", STARTED); + client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); + long duration = measureCertificateChoiceDuration(); + assertTrue(duration > 2000L, "Duration is " + duration); + assertTrue(duration < 3000L, "Duration is " + duration); + } + + @Test + public void setSessionStatusResponseSocketTimeout() { + client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); + SmartIdSignature signature = createSignature(); + assertNotNull(signature); + verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=10000"))); + } + + @Test + public void authenticateUsingDocumentNumber() { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + assertEquals("4430", authenticationHash.calculateVerificationCode()); + + SmartIdAuthenticationResponse authenticationResponse = client + .createAuthentication() + .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); + assertAuthenticationResponseValid(authenticationResponse); + } + + @Test + public void authenticate_usingSemanticsIdentifier() { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + assertEquals("4430", authenticationHash.calculateVerificationCode()); + + SmartIdAuthenticationResponse authenticationResponse = client + .createAuthentication() + .withSemanticsIdentifierAsString("PNOEE-31111111111") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertAuthenticationResponseValid(authenticationResponse); + } + + @Test + public void authenticateWithNonce() { + stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + assertEquals("4430", authenticationHash.calculateVerificationCode()); + + SmartIdAuthenticationResponse authenticationResponse = client + .createAuthentication() + .withDocumentNumber("PNOEE-31111111111-WITH-NONCE") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withNonce("g9rp4kjca3") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertAuthenticationResponseValid(authenticationResponse); + } + + @Test + public void authenticateWithManualSessionStatusRequesting() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + assertEquals("4430", authenticationHash.calculateVerificationCode()); + + AuthenticationRequestBuilder builder = client.createAuthentication(); + String sessionId = builder + .withSemanticsIdentifier(semanticsIdentifier) + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .initiateAuthentication(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); + + assertAuthenticationResponseValid(authenticationResponse); + verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb"))); + } + + @Test + public void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + assertEquals("4430", authenticationHash.calculateVerificationCode()); + + client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); + AuthenticationRequestBuilder builder = client.createAuthentication(); + String sessionId = builder + .withSemanticsIdentifier(semanticsIdentifier) + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .initiateAuthentication(); + + SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); + SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); + + assertAuthenticationResponseValid(authenticationResponse); + verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb?timeoutMs=5000"))); + } + + @Test + public void authenticate_whenUserAccountNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenUserCancels_shouldThrowException() { + assertThrows(UserRefusedException.class, () -> { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenUserRefusedGeneral.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenTimeout_shouldThrowException() { + assertThrows(SessionTimeoutException.class, () -> { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenTimeout.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenDocumentUnusable_shouldThrowException() { + assertThrows(DocumentUnusableException.class, () -> { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenDocumentUnusable.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 480); + makeAuthenticationRequest(); + }); + } + + @Test + public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 580); + makeAuthenticationRequest(); + }); + } + + @Test + public void setPollingSleepTimeoutForAuthentication() { + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); + client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); + long duration = measureAuthenticationDuration(); + assertTrue(duration > 2000L, "Duration is " + duration); + assertTrue(duration < 3000L, "Duration is " + duration); + } + + + @Test + public void getDeviceIpAddress_ipAddressNotPresent() { + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); + + SmartIdAuthenticationResponse authentication = createAuthentication(); + assertThat(authentication.getDeviceIpAddress(), is(nullValue())); + } + + @Test + public void getDeviceIpAddress_ipAddressReturned() { + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); + + SmartIdAuthenticationResponse authentication = createAuthentication(); + assertThat(authentication.getDeviceIpAddress(), is("62.65.42.45")); + } + + @Test + public void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + + String headerName = "custom-header"; + String headerValue = "Hi!"; + + Map headersToAdd = new HashMap<>(); + headersToAdd.put(headerName, headerValue); + ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headersToAdd); + client.setNetworkConnectionConfig(clientConfig); + makeAuthenticationRequest(); + + verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-32222222222-Z1B2-Q")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() { + String headerName = "custom-header"; + String headerValue = "Hello?!"; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); + client.setNetworkConnectionConfig(clientConfig); + makeCreateSignatureRequest(); + + verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-31111111111")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCustomHeader() { + String headerName = "custom-header"; + String headerValue = "Man, come on.."; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); + client.setNetworkConnectionConfig(clientConfig); + makeGetCertificateRequest(); + + verify(postRequestedFor(urlEqualTo("/certificatechoice/etsi/PNOEE-31111111111")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void verifySmartIdConnector_whenConnectorIsNotProvided() { + SmartIdConnector smartIdConnector = client.getSmartIdConnector(); + assertInstanceOf(SmartIdRestConnector.class, smartIdConnector); + } + + @Test + public void verifySmartIdConnector_whenConnectorIsProvided() { + final String mock = "MOCK"; + SessionStatus status = mock(SessionStatus.class); + when(status.getState()).thenReturn(mock); + SmartIdConnector connector = mock(SmartIdConnector.class); + when(connector.getSessionStatus(null)).thenReturn(status); + client.setSmartIdConnector(connector); + assertEquals(mock, client.getSmartIdConnector().getSessionStatus(null).getState()); + } + + @Test + public void getCertificate_noIdentifierGiven() { + assertThrows(SmartIdClientException.class, () -> + client + .getCertificate() + .withCertificateLevel("ADVANCED") + .fetch() + ); + } + + @Test + public void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { + SmartIdCertificate cer = client + .getCertificate() + .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .fetch(); + + assertCertificateResponseValid(cer); + } + + @Test + public void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldReturnValidCertificate() { + SmartIdCertificate cer = client + .getCertificate() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + .withCertificateLevel("ADVANCED") + .fetch(); + + assertCertificateResponseValid(cer); + } + + @Test + public void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { + SmartIdCertificate cer = client + .getCertificate() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + .withCertificateLevel("ADVANCED") + .fetch(); + + assertCertificateResponseValid(cer); + } + + @Test + public void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authResponse = client + .createAuthentication() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .withAuthenticationHash(authenticationHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertAuthenticationResponseValid(authResponse); + } + + @Test + public void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authResponse = client + .createAuthentication() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + .withCertificateLevel("ADVANCED") + .withAuthenticationHash(authenticationHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertAuthenticationResponseValid(authResponse); + } + + @Test + public void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + SmartIdAuthenticationResponse authResponse = client + .createAuthentication() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + .withCertificateLevel("ADVANCED") + .withAuthenticationHash(authenticationHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + + assertAuthenticationResponseValid(authResponse); + } + + @Test + public void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + + SignableHash signableHash = new SignableHash(); + signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + signableHash.setHashType(HashType.SHA256); + + SmartIdSignature signResponse = client + .createSignature() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .withSignableHash(signableHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signResponse); + } + + @Test + public void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + + SignableHash signableHash = new SignableHash(); + signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + signableHash.setHashType(HashType.SHA256); + + SmartIdSignature signResponse = client + .createSignature() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + .withCertificateLevel("ADVANCED") + .withSignableHash(signableHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signResponse); + } + + @Test + public void getSignatureByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + + SignableHash signableHash = new SignableHash(); + signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + signableHash.setHashType(HashType.SHA256); + + SmartIdSignature signResponse = client + .createSignature() + .withSemanticsIdentifier( + new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + .withCertificateLevel("ADVANCED") + .withSignableHash(signableHash) + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + + assertValidSignatureCreated(signResponse); + } + + private long measureSigningDuration() { + long startTime = System.currentTimeMillis(); + SmartIdSignature signature = createSignature(); + long endTime = System.currentTimeMillis(); + assertNotNull(signature); + return endTime - startTime; + } + + private SmartIdSignature createSignature() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + return client + .createSignature() + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + } + + private long measureAuthenticationDuration() { + long startTime = System.currentTimeMillis(); + SmartIdAuthenticationResponse AuthenticationResponse = createAuthentication(); + long endTime = System.currentTimeMillis(); + assertNotNull(AuthenticationResponse); + return endTime - startTime; + } + + private SmartIdAuthenticationResponse createAuthentication() { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + return client + .createAuthentication() + .withDocumentNumber("PNOEE-31111111111") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + } + + private long measureCertificateChoiceDuration() { + long startTime = System.currentTimeMillis(); + SmartIdCertificate certificate = client + .getCertificate() + .withDocumentNumber("PNOEE-31111111111") + .withCertificateLevel("ADVANCED") + .fetch(); + long endTime = System.currentTimeMillis(); + assertNotNull(certificate); + return endTime - startTime; + } + + private void makeGetCertificateRequest() { + client + .getCertificate() + .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withCertificateLevel("ADVANCED") + .fetch(); + } + + private void makeCreateSignatureRequest() { + SignableHash hashToSign = new SignableHash(); + hashToSign.setHashType(HashType.SHA256); + hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + + client + .createSignature() + .withDocumentNumber("PNOEE-31111111111") + .withSignableHash(hashToSign) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ) + .sign(); + } + + private void makeAuthenticationRequest() { + AuthenticationHash authenticationHash = new AuthenticationHash(); + authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + authenticationHash.setHashType(HashType.SHA512); + + client + .createAuthentication() + .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel("ADVANCED") + .withAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ) + .authenticate(); + } + + private ClientConfig getClientConfigWithCustomRequestHeaders(Map headers) { + ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); + clientConfig.register(new ClientRequestHeaderFilter(headers)); + return clientConfig; + } + + private void assertCertificateResponseValid(SmartIdCertificate certificate) { + assertNotNull(certificate); + assertNotNull(certificate.getCertificate()); + String name = certificate.getCertificate().getSubjectX500Principal().getName(); + assertThat(getAttribute(name, BCStyle.SERIALNUMBER), is("PNOEE-31111111111")); + assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); + assertEquals("QUALIFIED", certificate.getCertificateLevel()); + } + + private void assertValidSignatureCreated(SmartIdSignature signature) { + assertNotNull(signature); + assertThat(signature.getValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); + assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); + assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); + } + + private void assertAuthenticationResponseValid(SmartIdAuthenticationResponse authenticationResponse) { + assertNotNull(authenticationResponse); + assertEquals("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", authenticationResponse.getSignedHashInBase64()); + assertEquals("OK", authenticationResponse.getEndResult()); + assertNotNull(authenticationResponse.getCertificate()); + assertThat(authenticationResponse.getSignatureValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); + assertEquals("sha256WithRSAEncryption", authenticationResponse.getAlgorithmName()); + assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); + } + + private static String getAttribute(String name, ASN1ObjectIdentifier oid) { + X500Name x500name = new X500Name(name); + RDN[] rdns = x500name.getRDNs(oid); + return IETFUtils.valueToString(rdns[0].getFirst().getValue()); + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 1fcec122..fb454a3b 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,95 +26,100 @@ * #L% */ +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.junit.Assert.assertNotNull; - public class SmartIdRestServiceStubs { - public static void stubNotFoundResponse(String urlEquals) { - stubFor(get(urlEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } - - public static void stubNotFoundResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 404); - } - - public static void stubUnauthorizedResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 401); - } + public static void stubNotFoundResponse(String urlEquals) { + stubFor(get(urlEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } - public static void stubBadRequestResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 400); - } + public static void stubNotFoundResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 404); + } - public static void stubForbiddenResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 403); - } + public static void stubUnauthorizedResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 401); + } - public static void stubErrorResponse(String url, String requestFile, int errorStatus) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile))) - .willReturn(aResponse() - .withStatus(errorStatus) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } + public static void stubBadRequestResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 400); + } - public static void stubRequestWithResponse(String urlEquals, String responseFile) { - stubFor(get(urlPathEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } + public static void stubForbiddenResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 403); + } - public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile))) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } + public static void stubErrorResponse(String url, String requestFile, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile))) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } - public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { - String urlEquals = "/session/" + sessionId; - stubFor(get(urlEqualTo(urlEquals)) - .inScenario("session status") - .whenScenarioStateIs(startState) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile))) - .willSetStateTo(endState) - ); - } + public static void stubRequestWithResponse(String urlEquals, String responseFile) { + stubFor(get(urlPathEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } - private static String readFileBody(String fileName) { - ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull("File not found: " + fileName, resource); - File file = new File(resource.getFile()); - try { - return new String ( Files.readAllBytes( file.toPath() ), "UTF-8" ); + public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); } - catch (IOException e) { - throw new RuntimeException(e); + + public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { + String urlEquals = "/session/" + sessionId; + stubFor(get(urlEqualTo(urlEquals)) + .inScenario("session status") + .whenScenarioStateIs(startState) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile))) + .willSetStateTo(endState) + ); } - } + private static String readFileBody(String fileName) { + ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + File file = new File(resource.getFile()); + try { + return Files.readString(file.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java b/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java index bc08c72a..0557e112 100644 --- a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,32 +26,36 @@ * #L% */ -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; public class SmartIdSignatureTest { - @Test - public void getSignatureValueInBase64() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("VGVyZSBNYWFpbG0="); - assertEquals("VGVyZSBNYWFpbG0=", signature.getValueInBase64()); - } - - @Test - public void getSignatureValueInBytes() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("RGVkZ2Vob2c="); - assertArrayEquals("Dedgehog".getBytes(), signature.getValue()); - } - - @Test(expected = UnprocessableSmartIdResponseException.class) - public void incorrectBase64StringShouldThrowException() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("äIsNotValidBase64Character"); - signature.getValue(); - } + @Test + public void getSignatureValueInBase64() { + SmartIdSignature signature = new SmartIdSignature(); + signature.setValueInBase64("VGVyZSBNYWFpbG0="); + assertEquals("VGVyZSBNYWFpbG0=", signature.getValueInBase64()); + } + + @Test + public void getSignatureValueInBytes() { + SmartIdSignature signature = new SmartIdSignature(); + signature.setValueInBase64("RGVkZ2Vob2c="); + assertArrayEquals("Dedgehog".getBytes(), signature.getValue()); + } + + @Test + public void incorrectBase64StringShouldThrowException() { + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + SmartIdSignature signature = new SmartIdSignature(); + signature.setValueInBase64("äIsNotValidBase64Character"); + signature.getValue(); + }); + } } diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index 14ab494f..3dc81eb9 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,33 +26,35 @@ * #L% */ -import org.junit.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.charset.StandardCharsets; -import static org.junit.Assert.assertEquals; +import org.junit.jupiter.api.Test; + public class VerificationCodeCalculatorTest { - @Test - public void getVerificationCode() { - byte[] dummyDocumentHash = new byte[]{27, -69}; - String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); - assertEquals("4555", verificationCode); - } - - @Test - public void calculateCorrectVerificationCode() { - assertVerificationCode("7712", "Hello World!"); - assertVerificationCode("4612", "Hedgehogs – why can't they just share the hedge?"); - assertVerificationCode("7782", "Go ahead, make my day."); - assertVerificationCode("1464", "You're gonna need a bigger boat."); - assertVerificationCode("4240", "Say 'hello' to my little friend!"); - } - - private void assertVerificationCode(String verificationCode, String dataString) { - byte[] data = dataString.getBytes(StandardCharsets.UTF_8); - byte[] hash = DigestCalculator.calculateDigest(data, HashType.SHA256); - assertEquals(verificationCode, VerificationCodeCalculator.calculate(hash)); - } + @Test + public void getVerificationCode() { + byte[] dummyDocumentHash = new byte[]{27, -69}; + String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); + assertEquals("4555", verificationCode); + } + + @Test + public void calculateCorrectVerificationCode() { + assertVerificationCode("7712", "Hello World!"); + assertVerificationCode("4612", "Hedgehogs – why can't they just share the hedge?"); + assertVerificationCode("7782", "Go ahead, make my day."); + assertVerificationCode("1464", "You're gonna need a bigger boat."); + assertVerificationCode("4240", "Say 'hello' to my little friend!"); + } + + private void assertVerificationCode(String verificationCode, String dataString) { + byte[] data = dataString.getBytes(StandardCharsets.UTF_8); + byte[] hash = DigestCalculator.calculateDigest(data, HashType.SHA256); + assertEquals(verificationCode, VerificationCodeCalculator.calculate(hash)); + } } diff --git a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java index 175a3515..d72b2951 100644 --- a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,178 +26,194 @@ * #L% */ -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.*; -import org.junit.Before; -import org.junit.Test; - -import javax.net.ssl.SSLContext; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - import static ee.sk.smartid.DummyData.createSessionEndResult; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.rest.dao.CertificateRequest; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionStatusRequest; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.SignatureSessionResponse; public class SessionStatusPollerTest { - private SmartIdConnectorStub connector; - private SessionStatusPoller poller; - - @Before - public void setUp() { - connector = new SmartIdConnectorStub(); - poller = new SessionStatusPoller(connector); - poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 1L); - } - - @Test - public void getFirstCompleteResponse() { - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - assertEquals(1, connector.responseNumber); - assertCompleteStateReceived(status); - } - - @Test - public void pollAndGetThirdCompleteResponse() { - connector.responses.add(createRunningSessionStatus()); - connector.responses.add(createRunningSessionStatus()); - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertEquals(3, connector.responseNumber); - assertCompleteStateReceived(status); - } - - @Test - public void setPollingSleepTime() { - poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 200L); - addMultipleRunningSessionResponses(5); - connector.responses.add(createCompleteSessionStatus()); - long duration = measurePollingDuration(); - assertThat(duration, is(greaterThanOrEqualTo(1000L))); - assertThat(duration, is(lessThanOrEqualTo(1500L))); - } - - @Test - public void setResponseSocketOpenTime() { - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.MINUTES, 2L); - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertCompleteStateReceived(status); - assertTrue(connector.requestUsed.isResponseSocketOpenTimeSet()); - assertEquals(TimeUnit.MINUTES, connector.requestUsed.getResponseSocketOpenTimeUnit()); - assertEquals(2L, connector.requestUsed.getResponseSocketOpenTimeValue()); - } - - @Test - public void responseSocketOpenTimeShouldNotBeSetByDefault() { - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertCompleteStateReceived(status); - assertFalse(connector.requestUsed.isResponseSocketOpenTimeSet()); - } - - private long measurePollingDuration() { - long startTime = System.currentTimeMillis(); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - long endTime = System.currentTimeMillis(); - assertCompleteStateReceived(status); - return endTime - startTime; - } - - private void addMultipleRunningSessionResponses(int numberOfResponses) { - for (int i = 0; i < numberOfResponses; i++) - connector.responses.add(createRunningSessionStatus()); - } - - private void assertCompleteStateReceived(SessionStatus status) { - assertNotNull(status); - assertEquals("COMPLETE", status.getState()); - } - - private SessionStatus createCompleteSessionStatus() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(createSessionEndResult()); - return sessionStatus; - } - - private SessionStatus createRunningSessionStatus() { - SessionStatus status = new SessionStatus(); - status.setState("RUNNING"); - return status; - } - - public static class SmartIdConnectorStub implements SmartIdConnector { - String sessionIdUsed; - SessionStatusRequest requestUsed; - List responses = new ArrayList<>(); - int responseNumber = 0; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - sessionIdUsed = sessionId; - requestUsed = createSessionStatusRequest(sessionId); - return responses.get(responseNumber++); + private SmartIdConnectorStub connector; + private SessionStatusPoller poller; + + @BeforeEach + public void setUp() { + connector = new SmartIdConnectorStub(); + poller = new SessionStatusPoller(connector); + poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 1L); } - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; + + @Test + public void getFirstCompleteResponse() { + connector.responses.add(createCompleteSessionStatus()); + SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); + assertEquals(1, connector.responseNumber); + assertCompleteStateReceived(status); } - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - return null; + @Test + public void pollAndGetThirdCompleteResponse() { + connector.responses.add(createRunningSessionStatus()); + connector.responses.add(createRunningSessionStatus()); + connector.responses.add(createCompleteSessionStatus()); + SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + assertEquals(3, connector.responseNumber); + assertCompleteStateReceived(status); } - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, - CertificateRequest request) { - return null; + @Test + public void setPollingSleepTime() { + poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 200L); + addMultipleRunningSessionResponses(5); + connector.responses.add(createCompleteSessionStatus()); + long duration = measurePollingDuration(); + assertThat(duration, is(greaterThanOrEqualTo(1000L))); + assertThat(duration, is(lessThanOrEqualTo(1500L))); } - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - return null; + @Test + public void setResponseSocketOpenTime() { + connector.setSessionStatusResponseSocketOpenTime(TimeUnit.MINUTES, 2L); + connector.responses.add(createCompleteSessionStatus()); + SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + assertCompleteStateReceived(status); + assertTrue(connector.requestUsed.isResponseSocketOpenTimeSet()); + assertEquals(TimeUnit.MINUTES, connector.requestUsed.getResponseSocketOpenTimeUnit()); + assertEquals(2L, connector.requestUsed.getResponseSocketOpenTimeValue()); } - @Override - public SignatureSessionResponse sign(SemanticsIdentifier identifier, - SignatureSessionRequest request) { - return null; + @Test + public void responseSocketOpenTimeShouldNotBeSetByDefault() { + connector.responses.add(createCompleteSessionStatus()); + SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + assertCompleteStateReceived(status); + assertFalse(connector.requestUsed.isResponseSocketOpenTimeSet()); } - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - return null; + private long measurePollingDuration() { + long startTime = System.currentTimeMillis(); + SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); + long endTime = System.currentTimeMillis(); + assertCompleteStateReceived(status); + return endTime - startTime; } - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, - AuthenticationSessionRequest request) { - return null; + private void addMultipleRunningSessionResponses(int numberOfResponses) { + for (int i = 0; i < numberOfResponses; i++) { + connector.responses.add(createRunningSessionStatus()); + } } - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - SessionStatusRequest request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; + private void assertCompleteStateReceived(SessionStatus status) { + assertNotNull(status); + assertEquals("COMPLETE", status.getState()); } - @Override - public void setSslContext(SSLContext sslContext) { + private SessionStatus createCompleteSessionStatus() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(createSessionEndResult()); + return sessionStatus; + } + + private SessionStatus createRunningSessionStatus() { + SessionStatus status = new SessionStatus(); + status.setState("RUNNING"); + return status; + } + public static class SmartIdConnectorStub implements SmartIdConnector { + + private String sessionIdUsed; + private SessionStatusRequest requestUsed; + private final List responses = new ArrayList<>(); + int responseNumber = 0; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + private long sessionStatusResponseSocketOpenTimeValue; + + @Override + public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { + sessionIdUsed = sessionId; + requestUsed = createSessionStatusRequest(sessionId); + return responses.get(responseNumber++); + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; + } + + @Override + public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { + return null; + } + + @Override + public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, + CertificateRequest request) { + return null; + } + + @Override + public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { + return null; + } + + @Override + public SignatureSessionResponse sign(SemanticsIdentifier identifier, + SignatureSessionRequest request) { + return null; + } + + @Override + public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { + return null; + } + + @Override + public AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, + AuthenticationSessionRequest request) { + return null; + } + + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + SessionStatusRequest request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; + } + + @Override + public void setSslContext(SSLContext sslContext) { + + } } - } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index db590eb2..e48794e5 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,659 +26,721 @@ * #L% */ -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import ee.sk.smartid.ClientRequestHeaderFilter; -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.*; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.hamcrest.MatcherAssert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubBadRequestResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubUnauthorizedResponse; +import static java.util.Arrays.asList; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static ee.sk.smartid.SmartIdRestServiceStubs.*; -import static java.util.Arrays.asList; -import static org.hamcrest.core.StringStartsWith.startsWith; -import static org.junit.Assert.*; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.ClientRequestHeaderFilter; +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.rest.dao.CertificateRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.SignatureSessionResponse; + +@WireMockTest(httpPort = 18089) public class SmartIdRestConnectorTest { - @Rule - public WireMockRule wireMockRule = new WireMockRule(18089); - private SmartIdConnector connector; - - @Before - public void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test(expected = SessionNotFoundException.class) - public void getNotExistingSessionStatus() { - stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } - - @Test - public void getRunningSessionStatus() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunning.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - } - - @Test - public void getRunningSessionStatus_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunningWithIgnoredProperties.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - assertNotNull(sessionStatus.getIgnoredProperties()); - assertEquals(2, sessionStatus.getIgnoredProperties().length); - assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); - assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); - } - - @Test - public void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulCertificateRequest.json"); - assertSuccessfulResponse(sessionStatus); - assertNotNull(sessionStatus.getCert()); - MatcherAssert.assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - public void getSessionStatus_forSuccessfulSigningRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); - assertSuccessfulResponse(sessionStatus); - assertNotNull(sessionStatus.getSignature()); - MatcherAssert.assertThat(sessionStatus.getSignature().getValue(), startsWith("luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv")); - assertEquals("sha256WithRSAEncryption", sessionStatus.getSignature().getAlgorithm()); - } - - @Test - public void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); - assertSuccessfulResponse(sessionStatus); - - verify(getRequestedFor(urlMatching("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - public void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); - } - - @Test - public void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); - } - - @Test - public void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - } - - @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); - } - - @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); - } - - @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); - } - - @Test - public void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenTimeout.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); - } - - @Test - public void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); - } - - @Test - public void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenDocumentUnusable.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); - } - - @Test - public void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - assertSuccessfulResponse(sessionStatus); - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); - } - - @Test - public void getCertificate_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - public void getCertificate_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PASKZ-987654321012"); - - CertificateRequest request = createDummyCertificateRequest(); - CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - public void getCertificate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - request.setNonce("zstOt2umlc"); - CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - public void getCertificate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, "CZ", "1234567890"); - CertificateRequest request = createDummyCertificateRequest(); - request.setNonce("zstOt2umlc"); - CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test(expected = UserAccountNotFoundException.class) - public void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { - stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test(expected = UserAccountNotFoundException.class) - public void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { - stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequest.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("IDCCZ-1234567890"); - - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate(semanticsIdentifier, request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void getCertificate_withWrongAuthenticationParams_shuldThrowException() { - stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_withWrongRequestParams_shouldThrowException() { - stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void getCertificate_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 480); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test(expected = ServerMaintenanceException.class) - public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 580); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - } - - @Test - public void sign_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_hasUserAgentHeader() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - SignatureSessionResponse response = connector.sign("PNOEE-123456", createDummySignatureSessionRequest()); - assertNotNull(response); - - verify(postRequestedFor(urlMatching("/signature/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - public void sign_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - request.setNonce("zstOt2umlc"); - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessageInteraction = Interaction.confirmationMessage("Do you want to transfer 200 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackInteraction = Interaction.displayTextAndPIN("Transfer 200 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessageInteraction, fallbackInteraction)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_noFallback.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confi = Interaction.confirmationMessage("Do you want to transfer 999 Bison dollars from savings account to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(Collections.singletonList(confi)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction verificationCodeChoice = Interaction.verificationCodeChoice("Transfer 444 BSD to Oceanic Airlines?"); - Interaction fallbackToDisplayTextAndPIN = Interaction.displayTextAndPIN("Transfer 444 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(verificationCodeChoice, fallbackToDisplayTextAndPIN)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test(expected = UserAccountNotFoundException.class) - public void sign_whenDocumentNumberNotFound_shouldThrowException() { - stubNotFoundResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void sign_withWrongAuthenticationParams_shouldThrowException() { - stubUnauthorizedResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void sign_withWrongRequestParams_shouldThrowException() { - stubBadRequestResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void sign_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 480); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test(expected = ServerMaintenanceException.class) - public void sign_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 580); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - } - - @Test - public void authenticate_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - public void authenticate_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, "KZ", "987654321012"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - public void authenticate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setNonce("g9rp4kjca3"); - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - public void authenticate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "48308230504"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setNonce("g9rp4kjca3"); - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - - @Test - public void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOLT-48010010101"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - public void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - public void authenticate_hasUserAgentHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - connector.authenticate("PNOEE-123456", request); - - verify(postRequestedFor(urlMatching("/authentication/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test(expected = UserAccountNotFoundException.class) - public void authenticate_whenDocumentNumberNotFound_shouldThrowException() { - stubNotFoundResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test(expected = UserAccountNotFoundException.class) - public void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { - stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "requests/authenticationSessionRequest.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.LV, "230883-19894"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate(semanticsIdentifier, request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void authenticate_withWrongAuthenticationParams_shuldThrowException() { - stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void authenticate_withWrongRequestParams_shouldThrowException() { - stubBadRequestResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test(expected = RelyingPartyAccountConfigurationException.class) - public void authenticate_whenRequestForbidden_shouldThrowException() { - stubForbiddenResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test(expected = SmartIdClientException.class) - public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 480); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test(expected = ServerMaintenanceException.class) - public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { - stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 580); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - } - - @Test - public void verifyCustomRequestHeaderPresent_whenAuthenticating() { - String headerName = "custom-header"; - String headerValue = "Auth"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void verifyCustomRequestHeaderPresent_whenSigning() { - String headerName = "custom-header"; - String headerValue = "Sign"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { - String headerName = "custom-header"; - String headerValue = "Cert choice"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/certificatechoice/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - public void getCertificate_hasUserAgentHeader() { - connector = new SmartIdRestConnector("http://localhost:18089"); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - connector.getCertificate("PNOEE-123456", createDummyCertificateRequest()); - - verify(postRequestedFor(urlMatching("/certificatechoice/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - public void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { - String headerName = "custom-header"; - String headerValue = "Session status"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader(headerName, equalTo(headerValue))); - } - - private ClientConfig getClientConfigWithCustomRequestHeader(Map headers) { - ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); - clientConfig.register(new ClientRequestHeaderFilter(headers)); - return clientConfig; - } - - private void assertSuccessfulResponse(SessionStatus sessionStatus) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertNotNull(sessionStatus.getResult()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - assertEquals("PNOEE-31111111111", sessionStatus.getResult().getDocumentNumber()); - } - - private void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals(endResult, sessionStatus.getResult().getEndResult()); - } - - private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); - return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } - - private CertificateRequest createDummyCertificateRequest() { - CertificateRequest request = new CertificateRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - return request; - } - - private SignatureSessionRequest createDummySignatureSessionRequest() { - SignatureSessionRequest request = new SignatureSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - request.setHash("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - request.setHashType("SHA256"); - request.setAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ); - return request; - } - - private AuthenticationSessionRequest createDummyAuthenticationSessionRequest() { - AuthenticationSessionRequest request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - request.setHash("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - request.setHashType("SHA512"); - request.setAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ); - return request; - } + private SmartIdConnector connector; + + @BeforeEach + public void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + public void getNotExistingSessionStatus() { + assertThrows(SessionNotFoundException.class, () -> { + stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); + connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + }); + } + + @Test + public void getRunningSessionStatus() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunning.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + } + + @Test + public void getRunningSessionStatus_withIgnoredProperties() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunningWithIgnoredProperties.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + assertNotNull(sessionStatus.getIgnoredProperties()); + assertEquals(2, sessionStatus.getIgnoredProperties().length); + assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); + assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); + } + + @Test + public void getSessionStatus_forSuccessfulCertificateRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulCertificateRequest.json"); + assertSuccessfulResponse(sessionStatus); + assertNotNull(sessionStatus.getCert()); + MatcherAssert.assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + public void getSessionStatus_forSuccessfulSigningRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); + assertSuccessfulResponse(sessionStatus); + assertNotNull(sessionStatus.getSignature()); + MatcherAssert.assertThat(sessionStatus.getSignature().getValue(), startsWith("luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv")); + assertEquals("sha256WithRSAEncryption", sessionStatus.getSignature().getAlgorithm()); + } + + @Test + public void getSessionStatus_hasUserAgentHeader() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); + assertSuccessfulResponse(sessionStatus); + + verify(getRequestedFor(urlMatching("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + public void getSessionStatus_userHasRefused() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); + } + + @Test + public void getSessionStatus_userHasRefusedConfirmationMessage() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); + } + + @Test + public void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); + } + + @Test + public void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); + } + + @Test + public void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); + } + + @Test + public void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); + } + + @Test + public void getSessionStatus_timeout() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenTimeout.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); + } + + @Test + public void getSessionStatus_userHasSelectedWrongVcCode() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); + } + + @Test + public void getSessionStatus_whenDocumentUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenDocumentUnusable.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); + } + + @Test + public void getSessionStatus_withTimeoutParameter() { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); + connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); + SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + assertSuccessfulResponse(sessionStatus); + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); + } + + @Test + public void getCertificate_usingDocumentNumber() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + CertificateRequest request = createDummyCertificateRequest(); + CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); + assertNotNull(response); + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); + } + + @Test + public void getCertificate_usingSemanticsIdentifier() { + stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PASKZ-987654321012"); + + CertificateRequest request = createDummyCertificateRequest(); + CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); + assertNotNull(response); + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); + } + + @Test + public void getCertificate_withNonce_usingDocumentNumber() { + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + CertificateRequest request = createDummyCertificateRequest(); + request.setNonce("zstOt2umlc"); + CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); + assertNotNull(response); + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); + } + + @Test + public void getCertificate_withNonce_usingSemanticsIdentifier() { + stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, "CZ", "1234567890"); + CertificateRequest request = createDummyCertificateRequest(); + request.setNonce("zstOt2umlc"); + CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); + assertNotNull(response); + assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); + } + + @Test + public void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequest.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("IDCCZ-1234567890"); + + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate(semanticsIdentifier, request); + }); + } + + @Test + public void getCertificate_withWrongAuthenticationParams_shuldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void getCertificate_withWrongRequestParams_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void getCertificate_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 480); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 580); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + }); + } + + @Test + public void sign_usingDocumentNumber() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_hasUserAgentHeader() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + SignatureSessionResponse response = connector.sign("PNOEE-123456", createDummySignatureSessionRequest()); + assertNotNull(response); + + verify(postRequestedFor(urlMatching("/signature/document/PNOEE-123456")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + public void sign_withNonce_usingDocumentNumber() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + request.setNonce("zstOt2umlc"); + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + + Interaction confirmationMessageInteraction = Interaction.confirmationMessage("Do you want to transfer 200 Bison dollars from savings account to Oceanic Airlines?"); + Interaction fallbackInteraction = Interaction.displayTextAndPIN("Transfer 200 BSD to Oceanic Airlines?"); + request.setAllowedInteractionsOrder(asList(confirmationMessageInteraction, fallbackInteraction)); + + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_noFallback.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + + Interaction confi = Interaction.confirmationMessage("Do you want to transfer 999 Bison dollars from savings account to Oceanic Airlines?"); + request.setAllowedInteractionsOrder(Collections.singletonList(confi)); + + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + + Interaction verificationCodeChoice = Interaction.verificationCodeChoice("Transfer 444 BSD to Oceanic Airlines?"); + Interaction fallbackToDisplayTextAndPIN = Interaction.displayTextAndPIN("Transfer 444 BSD to Oceanic Airlines?"); + request.setAllowedInteractionsOrder(asList(verificationCodeChoice, fallbackToDisplayTextAndPIN)); + + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + + Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); + Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); + request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); + + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + + Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); + Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); + request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); + + SignatureSessionResponse response = connector.sign("PNOEE-123456", request); + assertNotNull(response); + assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); + } + + @Test + public void sign_whenDocumentNumberNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void sign_withWrongAuthenticationParams_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubUnauthorizedResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void sign_withWrongRequestParams_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubBadRequestResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void sign_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 480); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void sign_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 580); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_usingDocumentNumber() { + stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + @Test + public void authenticate_usingSemanticsIdentifier() { + stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, "KZ", "987654321012"); + + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + @Test + public void authenticate_withNonce_usingDocumentNumber() { + stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + request.setNonce("g9rp4kjca3"); + AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + @Test + public void authenticate_withNonce_usingSemanticsIdentifier() { + stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "48308230504"); + + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + request.setNonce("g9rp4kjca3"); + AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + + @Test + public void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { + stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOLT-48010010101"); + + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); + + AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + @Test + public void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { + stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); + + AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); + assertNotNull(response); + assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); + } + + @Test + public void authenticate_hasUserAgentHeader() { + stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); + + connector.authenticate("PNOEE-123456", request); + + verify(postRequestedFor(urlMatching("/authentication/document/PNOEE-123456")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + public void authenticate_whenDocumentNumberNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { + assertThrows(UserAccountNotFoundException.class, () -> { + stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "requests/authenticationSessionRequest.json"); + + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.LV, "230883-19894"); + + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate(semanticsIdentifier, request); + }); + } + + @Test + public void authenticate_withWrongAuthenticationParams_shuldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_withWrongRequestParams_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubBadRequestResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_whenRequestForbidden_shouldThrowException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + stubForbiddenResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + assertThrows(SmartIdClientException.class, () -> { + stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 480); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { + assertThrows(ServerMaintenanceException.class, () -> { + stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 580); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + }); + } + + @Test + public void verifyCustomRequestHeaderPresent_whenAuthenticating() { + String headerName = "custom-header"; + String headerValue = "Auth"; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); + connector.authenticate("PNOEE-123456", request); + + verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-123456")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void verifyCustomRequestHeaderPresent_whenSigning() { + String headerName = "custom-header"; + String headerValue = "Sign"; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); + stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + SignatureSessionRequest request = createDummySignatureSessionRequest(); + connector.sign("PNOEE-123456", request); + + verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-123456")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { + String headerName = "custom-header"; + String headerValue = "Cert choice"; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + CertificateRequest request = createDummyCertificateRequest(); + connector.getCertificate("PNOEE-123456", request); + + verify(postRequestedFor(urlEqualTo("/certificatechoice/document/PNOEE-123456")) + .withHeader(headerName, equalTo(headerValue))); + } + + @Test + public void getCertificate_hasUserAgentHeader() { + connector = new SmartIdRestConnector("http://localhost:18089"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + connector.getCertificate("PNOEE-123456", createDummyCertificateRequest()); + + verify(postRequestedFor(urlMatching("/certificatechoice/document/PNOEE-123456")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + public void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { + String headerName = "custom-header"; + String headerValue = "Session status"; + + Map headers = new HashMap<>(); + headers.put(headerName, headerValue); + connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); + connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) + .withHeader(headerName, equalTo(headerValue))); + } + + private ClientConfig getClientConfigWithCustomRequestHeader(Map headers) { + ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); + clientConfig.register(new ClientRequestHeaderFilter(headers)); + return clientConfig; + } + + private void assertSuccessfulResponse(SessionStatus sessionStatus) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertNotNull(sessionStatus.getResult()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + assertEquals("PNOEE-31111111111", sessionStatus.getResult().getDocumentNumber()); + } + + private void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals(endResult, sessionStatus.getResult().getEndResult()); + } + + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); + return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + } + + private CertificateRequest createDummyCertificateRequest() { + CertificateRequest request = new CertificateRequest(); + request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + request.setRelyingPartyName("BANK123"); + request.setCertificateLevel("ADVANCED"); + return request; + } + + private SignatureSessionRequest createDummySignatureSessionRequest() { + SignatureSessionRequest request = new SignatureSessionRequest(); + request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + request.setRelyingPartyName("BANK123"); + request.setCertificateLevel("ADVANCED"); + request.setHash("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); + request.setHashType("SHA256"); + request.setAllowedInteractionsOrder(asList( + Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), + Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) + ); + return request; + } + + private AuthenticationSessionRequest createDummyAuthenticationSessionRequest() { + AuthenticationSessionRequest request = new AuthenticationSessionRequest(); + request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + request.setRelyingPartyName("BANK123"); + request.setCertificateLevel("ADVANCED"); + request.setHash("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); + request.setHashType("SHA512"); + request.setAllowedInteractionsOrder(asList( + Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), + Interaction.displayTextAndPIN("Log in?")) + ); + return request; + } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java index 6008d599..d7632c52 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,250 +26,260 @@ * #L% */ -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; -import ee.sk.smartid.rest.dao.*; -import org.apache.commons.codec.binary.Base64; -import org.junit.Before; -import org.junit.Test; +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.Collections; import java.util.concurrent.TimeUnit; -import static ee.sk.test.smartid.integration.SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO; -import static java.util.Arrays.asList; -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.Assume.assumeTrue; +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ee.sk.SmartIdDemoIntegrationTest; +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashType; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.rest.dao.CertificateRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.SignatureSessionResponse; + +@SmartIdDemoIntegrationTest public class SmartIdRestIntegrationTest { - private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String DOCUMENT_NUMBER = "PNOLT-30303039903-MOCK-Q"; - private static final String DOCUMENT_NUMBER_LT = "PNOLT-30303039914-MOCK-Q"; - private static final String DATA_TO_SIGN = "Hello World!"; - private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; - private SmartIdConnector connector; + private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final String DOCUMENT_NUMBER = "PNOEE-50609019996-MOCK-Q"; + private static final String DOCUMENT_NUMBER_LT = "PNOLT-50609019996-MOCK-Q"; + private static final String DATA_TO_SIGN = "Hello World!"; + private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; - @Before - public void setUp() { - connector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v2/"); + private SmartIdConnector connector; - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); - } + @BeforeEach + public void setUp() { + connector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v2/"); + } - @Test - public void getCertificateAndSignHash() throws Exception { - CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER_LT); + @Test + public void getCertificateAndSignHash() throws Exception { + CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER_LT); - SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); - assertCertificateChosen(sessionStatus); + SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); + assertCertificateChosen(sessionStatus); - String documentNumber = sessionStatus.getResult().getDocumentNumber(); - SignatureSessionResponse signatureSessionResponse = createRequestAndFetchSignatureSession(documentNumber); - sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); - assertSignatureCreated(sessionStatus); - } + String documentNumber = sessionStatus.getResult().getDocumentNumber(); + SignatureSessionResponse signatureSessionResponse = createRequestAndFetchSignatureSession(documentNumber); + sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); + assertSignatureCreated(sessionStatus); + } - @Test - public void authenticate_withSemanticsIdentifier() throws Exception { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); + @Test + public void authenticate_withSemanticsIdentifier() throws Exception { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, request); + AuthenticationSessionRequest request = createAuthenticationSessionRequest(); + AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, request); - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertNotNull(authenticationSessionResponse); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - assertAuthenticationResponseCreated(sessionStatus); - } + SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); + assertAuthenticationResponseCreated(sessionStatus); + } - @Test - public void authenticate_withDocumentNumber() throws Exception { - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, request); + @Test + public void authenticate_withDocumentNumber() throws Exception { + AuthenticationSessionRequest request = createAuthenticationSessionRequest(); + AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, request); - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertNotNull(authenticationSessionResponse); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); + SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); + assertNotNull(sessionStatus.getResult()); + assertThat(sessionStatus.getResult().getEndResult(), is("OK")); + assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - assertAuthenticationResponseCreated(sessionStatus); - } + assertAuthenticationResponseCreated(sessionStatus); + } - @Test - public void authenticate_withDocumentNumber_advancedInteraction() throws Exception { - AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); - authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - authenticationSessionRequest.setHashType("SHA512"); - authenticationSessionRequest.setHash(calculateHashInBase64(DATA_TO_SIGN.getBytes())); + @Test + public void authenticate_withDocumentNumber_advancedInteraction() throws Exception { + AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); + authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); + authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); + authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); + authenticationSessionRequest.setHashType("SHA512"); + authenticationSessionRequest.setHash(calculateHashInBase64(DATA_TO_SIGN.getBytes())); - authenticationSessionRequest.setAllowedInteractionsOrder( - asList(Interaction.confirmationMessage("Do you want to log in to internet banking system of Oceanic Bank?"), - Interaction.displayTextAndPIN("Log into internet banking system?"))); + authenticationSessionRequest.setAllowedInteractionsOrder( + asList(Interaction.confirmationMessage("Do you want to log in to internet banking system of Oceanic Bank?"), + Interaction.displayTextAndPIN("Log into internet banking system?"))); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, authenticationSessionRequest); + AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, authenticationSessionRequest); - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertNotNull(authenticationSessionResponse); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); + SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - org.hamcrest.MatcherAssert.assertThat(sessionStatus.getInteractionFlowUsed(), is("confirmationMessage")); + assertNotNull(sessionStatus.getResult()); + assertThat(sessionStatus.getResult().getEndResult(), is("OK")); + org.hamcrest.MatcherAssert.assertThat(sessionStatus.getInteractionFlowUsed(), is("confirmationMessage")); - assertAuthenticationResponseCreated(sessionStatus); - } + assertAuthenticationResponseCreated(sessionStatus); + } - //@Test CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES - public void getIgnoredProperties_withSign_getIgnoredProperties_withAuthenticate_testAccountsIgnoreVcChoice() throws Exception { - CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER); + //@Test CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES + public void getIgnoredProperties_withSign_getIgnoredProperties_withAuthenticate_testAccountsIgnoreVcChoice() throws Exception { + CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER); - SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); - assertCertificateChosen(sessionStatus); + SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); + assertCertificateChosen(sessionStatus); - String documentNumber = sessionStatus.getResult().getDocumentNumber(); + String documentNumber = sessionStatus.getResult().getDocumentNumber(); - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); + SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - SignatureSessionResponse signatureSessionResponse = fetchSignatureSession(documentNumber, signatureSessionRequest); - sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); + SignatureSessionResponse signatureSessionResponse = fetchSignatureSession(documentNumber, signatureSessionRequest); + sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); + assertNotNull(sessionStatus.getResult()); + assertThat(sessionStatus.getResult().getEndResult(), is("OK")); + assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - assertSignatureCreated(sessionStatus); - assertNotNull(sessionStatus.getIgnoredProperties()); + assertSignatureCreated(sessionStatus); + assertNotNull(sessionStatus.getIgnoredProperties()); - assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); - assertThat(sessionStatus.getIgnoredProperties().length, equalTo(2)); + assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); + assertThat(sessionStatus.getIgnoredProperties().length, equalTo(2)); - } + } - //@Test //CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES - public void getIgnoredProperties_withAuthenticate() throws Exception { - AuthenticationSessionRequest authenticationSessionRequest = createAuthenticationSessionRequest(); + //@Test //CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES + public void getIgnoredProperties_withAuthenticate() throws Exception { + AuthenticationSessionRequest authenticationSessionRequest = createAuthenticationSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, authenticationSessionRequest); + AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, authenticationSessionRequest); - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertNotNull(authenticationSessionResponse); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); + SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); + assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - assertAuthenticationResponseCreated(sessionStatus); - assertNotNull(sessionStatus.getIgnoredProperties()); + assertAuthenticationResponseCreated(sessionStatus); + assertNotNull(sessionStatus.getIgnoredProperties()); - assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); - } + assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); + } - private CertificateChoiceResponse fetchCertificateChoiceSession(String documentNumber) { - CertificateRequest request = createCertificateRequest(); - CertificateChoiceResponse certificateChoiceResponse = connector.getCertificate(documentNumber, request); - assertNotNull(certificateChoiceResponse); - assertThat(certificateChoiceResponse.getSessionID(), not(isEmptyOrNullString())); - return certificateChoiceResponse; - } + private CertificateChoiceResponse fetchCertificateChoiceSession(String documentNumber) { + CertificateRequest request = createCertificateRequest(); + CertificateChoiceResponse certificateChoiceResponse = connector.getCertificate(documentNumber, request); + assertNotNull(certificateChoiceResponse); + assertThat(certificateChoiceResponse.getSessionID(), not(emptyOrNullString())); + return certificateChoiceResponse; + } - private CertificateRequest createCertificateRequest() { - CertificateRequest request = new CertificateRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - return request; - } + private CertificateRequest createCertificateRequest() { + CertificateRequest request = new CertificateRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); + return request; + } - private SignatureSessionResponse createRequestAndFetchSignatureSession(String documentNumber) { - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - return fetchSignatureSession(documentNumber, signatureSessionRequest); - } + private SignatureSessionResponse createRequestAndFetchSignatureSession(String documentNumber) { + SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); + return fetchSignatureSession(documentNumber, signatureSessionRequest); + } - private SignatureSessionResponse fetchSignatureSession(String documentNumber, SignatureSessionRequest signatureSessionRequest) { - SignatureSessionResponse signatureSessionResponse = connector.sign(documentNumber, signatureSessionRequest); - assertThat(signatureSessionResponse.getSessionID(), not(isEmptyOrNullString())); - return signatureSessionResponse; - } + private SignatureSessionResponse fetchSignatureSession(String documentNumber, SignatureSessionRequest signatureSessionRequest) { + SignatureSessionResponse signatureSessionResponse = connector.sign(documentNumber, signatureSessionRequest); + assertThat(signatureSessionResponse.getSessionID(), not(emptyOrNullString())); + return signatureSessionResponse; + } - private SignatureSessionRequest createSignatureSessionRequest() { - SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); - signatureSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - signatureSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - signatureSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - signatureSessionRequest.setHashType("SHA512"); - String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); - signatureSessionRequest.setHash(hashInBase64); - signatureSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to bank?"))); - return signatureSessionRequest; - } + private SignatureSessionRequest createSignatureSessionRequest() { + SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); + signatureSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); + signatureSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); + signatureSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); + signatureSessionRequest.setHashType("SHA512"); + String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); + signatureSessionRequest.setHash(hashInBase64); + signatureSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to bank?"))); + return signatureSessionRequest; + } - public static AuthenticationSessionRequest createAuthenticationSessionRequest() { - AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); - authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - authenticationSessionRequest.setHashType("SHA512"); - String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); - authenticationSessionRequest.setHash(hashInBase64); + public static AuthenticationSessionRequest createAuthenticationSessionRequest() { + AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); + authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); + authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); + authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); + authenticationSessionRequest.setHashType("SHA512"); + String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); + authenticationSessionRequest.setHash(hashInBase64); - authenticationSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); + authenticationSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - return authenticationSessionRequest; - } + return authenticationSessionRequest; + } - public static SessionStatus pollSessionStatus(String sessionId, SmartIdConnector connector1) throws InterruptedException { - SessionStatus sessionStatus = null; - while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState() )) { - sessionStatus = connector1.getSessionStatus(sessionId); - TimeUnit.SECONDS.sleep(1); + public static SessionStatus pollSessionStatus(String sessionId, SmartIdConnector connector1) throws InterruptedException { + SessionStatus sessionStatus = null; + while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + sessionStatus = connector1.getSessionStatus(sessionId); + TimeUnit.SECONDS.sleep(1); + } + assertEquals("COMPLETE", sessionStatus.getState()); + return sessionStatus; } - assertEquals("COMPLETE", sessionStatus.getState()); - return sessionStatus; - } - - private void assertSignatureCreated(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - assertNotNull(sessionStatus.getSignature()); - assertThat(sessionStatus.getSignature().getValue(), not(isEmptyOrNullString())); - } - - private void assertCertificateChosen(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - String documentNumber = sessionStatus.getResult().getDocumentNumber(); - assertThat(documentNumber, not(isEmptyOrNullString())); - assertThat(sessionStatus.getCert().getValue(), not(isEmptyOrNullString())); - } - - public static void assertAuthenticationResponseCreated(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - - assertThat(sessionStatus.getResult().getEndResult(), not(isEmptyOrNullString())); - assertThat(sessionStatus.getSignature().getValue(), not(isEmptyOrNullString())); - assertThat(sessionStatus.getCert().getValue(), not(isEmptyOrNullString())); - assertThat(sessionStatus.getCert().getCertificateLevel(), not(isEmptyOrNullString())); - } - - private static String calculateHashInBase64(byte[] dataToSign) { - byte[] digestValue = DigestCalculator.calculateDigest(dataToSign, HashType.SHA512); - return Base64.encodeBase64String(digestValue); - } + private void assertSignatureCreated(SessionStatus sessionStatus) { + assertNotNull(sessionStatus); + assertNotNull(sessionStatus.getSignature()); + assertThat(sessionStatus.getSignature().getValue(), not(emptyOrNullString())); + } + + private void assertCertificateChosen(SessionStatus sessionStatus) { + assertNotNull(sessionStatus); + String documentNumber = sessionStatus.getResult().getDocumentNumber(); + assertThat(documentNumber, not(emptyOrNullString())); + assertThat(sessionStatus.getCert().getValue(), not(emptyOrNullString())); + } + + public static void assertAuthenticationResponseCreated(SessionStatus sessionStatus) { + assertNotNull(sessionStatus); + + assertThat(sessionStatus.getResult().getEndResult(), not(emptyOrNullString())); + assertThat(sessionStatus.getSignature().getValue(), not(emptyOrNullString())); + assertThat(sessionStatus.getCert().getValue(), not(emptyOrNullString())); + assertThat(sessionStatus.getCert().getCertificateLevel(), not(emptyOrNullString())); + } + + private static String calculateHashInBase64(byte[] dataToSign) { + byte[] digestValue = DigestCalculator.calculateDigest(dataToSign, HashType.SHA512); + return Base64.encodeBase64String(digestValue); + } } diff --git a/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java index bfea9fe0..d4027dc7 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,11 +26,12 @@ * #L% */ -import org.junit.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.Test; + public class SemanticsIdentifierTest { @Test diff --git a/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java b/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java index 4b5a5d1a..52249497 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java +++ b/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,13 +26,18 @@ * #L% */ -import org.junit.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; public class SignatureSessionRequestTest { - @Test(expected = UnsupportedOperationException.class) + @Test public void setDisplayText() { - SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); - signatureSessionRequest.setDisplayText("test"); + assertThrows(UnsupportedOperationException.class, () -> { + SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); + signatureSessionRequest.setDisplayText("test"); + }); } } diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index ef899ca8..bfdfee5e 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,21 +26,38 @@ * #L% */ -import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.LocalDate; +import java.util.Optional; +import java.util.stream.Stream; -import static ee.sk.smartid.AuthenticationResponseValidatorTest.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.CertificateUtil; public class CertificateAttributeUtilTest { + private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; + private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; + @Test public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { - X509Certificate certificateWithDob = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LV_WITH_DOB)); + X509Certificate certificateWithDob = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); @@ -50,11 +67,45 @@ public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws @Test public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { - X509Certificate certificateWithoutDobAttribute = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LV)); + X509Certificate certificateWithoutDobAttribute = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV); LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); assertThat(dateOfBirthCertificateAttribute, is(nullValue())); } + @ParameterizedTest + @ArgumentsSource(AttributeArgumentProvider.class) + void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); + + assertTrue(attributeValue.isPresent()); + assertThat(attributeValue.get(), is(expectedValue)); + } + + @Test + void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); + + assertTrue(attributeValue.isEmpty()); + } + + private static class AttributeArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Given name", BCStyle.GIVENNAME), "BOD"), + Arguments.of(Named.of("Surname", BCStyle.SURNAME), "TESTNUMBER"), + Arguments.of(Named.of("Serial number", BCStyle.SERIALNUMBER), "PNOLV-329999-99901"), + Arguments.of(Named.of("Country", BCStyle.C), "LV") + ); + } + } } diff --git a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java index 966d9ebe..36939763 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,30 +26,37 @@ * #L% */ -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationResponseValidator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.junit.Assert; -import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.LocalDate; -import static ee.sk.smartid.AuthenticationResponseValidatorTest.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.CertificateUtil; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; public class NationalIdentityNumberUtilTest { + private static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; + private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; + private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; + @Test public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { - - X509Certificate eeCertificate = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_EE)); + X509Certificate eeCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE); AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(eeCertificate); - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); assertThat(dateOfBirth, is(notNullValue())); @@ -58,7 +65,7 @@ public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws Certificate @Test public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { - X509Certificate lvCertificate = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903)); + X509Certificate lvCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(lvCertificate); @@ -70,7 +77,7 @@ public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateE @Test public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { - X509Certificate ltCertificate = getX509Certificate(getX509CertificateBytes(AUTH_CERTIFICATE_LT)); + X509Certificate ltCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LT); AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(ltCertificate); @@ -80,9 +87,10 @@ public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws Certifica assertThat(dateOfBirth, is(LocalDate.of(1960, 9, 6))); } - @Test - public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("321205-1234"); + @ParameterizedTest + @ValueSource(strings = {"321205-1234", "331205-1234", "341205-1234", "351205-1234", "361205-1234", "371205-1234", "381205-1234", "391205-1234"}) + public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull(String lvNationalIdentityNumber) { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth(lvNationalIdentityNumber); assertThat(birthDate, is(nullValue())); } @@ -106,20 +114,10 @@ public void parseLvDateOfBirth_19century() { @Test public void parseLvDateOfBirth_invalidMonth_throwsException() { - UnprocessableSmartIdResponseException exception = Assert.assertThrows(UnprocessableSmartIdResponseException.class, () -> { - NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234"); - }); - - assertThat(exception.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); - } - - @Test - public void parseLvDateOfBirth_invalidIdCode_throwsException() { - UnprocessableSmartIdResponseException exception = Assert.assertThrows(UnprocessableSmartIdResponseException.class, () -> { - NationalIdentityNumberUtil.parseLvDateOfBirth("331265-0234"); - }); + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, + () -> NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234")); - assertThat(exception.getMessage(), is("Unable get birthdate from Latvian personal code 331265-0234")); + assertThat(unprocessableSmartIdResponseException.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); } @Test diff --git a/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java index b83ef864..06a53353 100644 --- a/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,24 +26,15 @@ * #L% */ -import ee.sk.smartid.*; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; -import org.apache.http.client.config.RequestConfig; -import org.glassfish.jersey.apache.connector.ApacheClientProperties; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.hamcrest.CoreMatchers; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.assertAuthenticationResponseCreated; +import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.createAuthenticationSessionRequest; +import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.pollSessionStatus; +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.InputStream; import java.security.KeyStore; @@ -54,14 +45,41 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.*; -import static ee.sk.test.smartid.integration.SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO; -import static java.util.Arrays.asList; -import static junit.framework.TestCase.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isEmptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.junit.Assume.assumeTrue; +import org.apache.http.client.config.RequestConfig; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.FileUtil; +import ee.sk.SmartIdDemoIntegrationTest; +import ee.sk.smartid.AuthenticationHash; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.HashType; +import ee.sk.smartid.SignableHash; +import ee.sk.smartid.SmartIdAuthenticationResponse; +import ee.sk.smartid.SmartIdCertificate; +import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.SmartIdSignature; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.InteractionFlow; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; /** * These tests contain snippets used in Readme.md @@ -69,55 +87,19 @@ * If anything changes in this class (except setUp method) the changes must be reflected in Readme.md * These are not real tests! */ +@SmartIdDemoIntegrationTest public class ReadmeTest { private static final Logger logger = LoggerFactory.getLogger(ReadmeTest.class); - SmartIdClient client; - - SmartIdAuthenticationResponse authenticationResponse; - - SignableHash hashToSign; - - public static final String DEMO_HOST_SSL_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIIGoDCCBYigAwIBAgIQBOJYR4uzB/mihrGnWl+QIjANBgkqhkiG9w0BAQsFADBP\n" - + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE\n" - + "aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjA5MTYwMDAwMDBa\n" - + "Fw0yMzEwMTcyMzU5NTlaMFUxCzAJBgNVBAYTAkVFMRAwDgYDVQQHEwdUYWxsaW5u\n" - + "MRswGQYDVQQKExJTSyBJRCBTb2x1dGlvbnMgQVMxFzAVBgNVBAMTDnNpZC5kZW1v\n" - + "LnNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoDLLTK+NEKsB\n" - + "POdOEjAK7/A8JTmZXlRkjM1aX0pfH6BCIGn3ZJd9M6iSR+KKQEfT0cj7JWvfMjZT\n" - + "oVHxOPbUaIUTdu22akLDy0kuZN78/RdqHUPq9WTKZsG3r03bi6tGqFb2KfzhZ2Q9\n" - + "zfS8Yn5N0iPeMh48BsreEdumb4F97JSEzjzFdGBb5wED//pHUL2VRoX1hzKV/6D8\n" - + "/sWmbMdGTYcXds/JbOIFU6EgAO2ozJUQmTbR2XRJYawKYAm4CEyY49zzvOldjOUC\n" - + "VjbheCxPJB0OeqYmfxm6QNqEi33Jsof9Y8uRl/DrEGexApd0bQkcGoGyBB08MWyu\n" - + "xjjmjh6TSQIDAQABo4IDcDCCA2wwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iy\n" - + "xZV2ufQwHQYDVR0OBBYEFIrtybLjSa2jrMVWly+c7KCBvpifMBkGA1UdEQQSMBCC\n" - + "DnNpZC5kZW1vLnNrLmVlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF\n" - + "BQcDAQYIKwYBBQUHAwIwgY8GA1UdHwSBhzCBhDBAoD6gPIY6aHR0cDovL2NybDMu\n" - + "ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDBA\n" - + "oD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hB\n" - + "MjU2MjAyMENBMS00LmNybDA+BgNVHSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUF\n" - + "BwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwfwYIKwYBBQUHAQEEczBx\n" - + "MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSQYIKwYBBQUH\n" - + "MAKGPWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNI\n" - + "QTI1NjIwMjBDQTEtMS5jcnQwCQYDVR0TBAIwADCCAYAGCisGAQQB1nkCBAIEggFw\n" - + "BIIBbAFqAHcA6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGDRaWg\n" - + "0AAABAMASDBGAiEA0YjYuhVcbwncKefVPz4d8IrAQQ6ahcw5mOFufHTwbV8CIQCk\n" - + "oYVmHeYe9C9WeHYT4sKozs3ubeNqxPDRjKKaCPhtzQB2ADXPGRu/sWxXvw+tTG1C\n" - + "y7u2JyAmUeo/4SrvqAPDO9ZMAAABg0WloQQAAAQDAEcwRQIhALhRwut2GdVSxBnG\n" - + "KJOvCyaCySEhF7CXkhJRYsaZhBADAiB2X85UxwB5030w+1pX0QxJ4Z3A2sLwrwYR\n" - + "9/+yt4NGLwB3ALc++yTfnE26dfI5xbpY9Gxd/ELPep81xJ4dCYEl7bSZAAABg0Wl\n" - + "oRUAAAQDAEgwRgIhAPFc0KtyRqpNV3muD5aCzgE0RuQxsz6KPYKX4I49hfZeAiEA\n" - + "yuqiqCAtBkt/G7Wq4SA+/4xDyRKwXo5Zu8QuGGx9taYwDQYJKoZIhvcNAQELBQAD\n" - + "ggEBADTzrIM6pAvIClyXTGtyceDKckkGENmFmDvwL6I0Tab/s8uLlREpDhRPQpFQ\n" - + "hsAjaxWrfUv25EdYelBvaiOrCUwI3W3zlLy4gcgagEyTJ71lz7cH0VwFWjTsfXXc\n" - + "osD5sXMfipvkgmX+XgYJjsDY/HDFQyZp7aoTVqAlOfqkfsHi1EGdd6AGKP0yHokU\n" - + "3sUH1X6kDQdSfu1iwRPCn1CGS6xU1VJ6mJDU8SioBQKBAQkCs5UVdjdH+o99xsND\n" - + "8kfVHlchc+SxsI5cYhc4gUjjtX/U3FDZcW1IfZDil9tQf9l6rU/ZXMIPHeQWTPAa\n" - + "nUMrQKgVkBFH6CVchyHXPejDNGA=\n" - + "-----END CERTIFICATE-----"; - - @Before + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + + private SmartIdClient client; + + private SmartIdAuthenticationResponse authenticationResponse; + + private SignableHash hashToSign; + + @BeforeEach public void setUp() { client = new SmartIdClient(); client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); @@ -132,9 +114,6 @@ public void setUp() { // calculate hash from the document you want to sign (i.e. use DigiDoc4J or other libraries) // this class also has a method to set hash as bite array hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); } /* @@ -181,23 +160,24 @@ public void documentConfigureTheClient_trustStore() throws Exception { ### Feeding trusted certificates one by one - It also possible to feed trusted certificates one by one. + It is also possible to feed trusted certificates one by one. This can prove useful when trusted certificates are kept as application configuration property. */ - @Test(expected = SmartIdClientException.class) + @Test public void documentConfigureTheClient_feedSeparately() { - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setTrustedCertificates( - "-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj...", - "-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQbQr/Ky22GFhYWS3oQoJkyT..." - ); + assertThrows(SmartIdClientException.class, () -> { + SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); + client.setTrustedCertificates( + "-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj...", + "-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQbQr/Ky22GFhYWS3oQoJkyT..." + ); + }); } /* @@ -232,7 +212,7 @@ public void documentAuthenticatingWithSemanticsIdentifier() { // Smart-ID app will display verification code to the user and user must insert PIN1 .withAllowedInteractionsOrder( Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) + )) // we want to get the IP address of the device running Smart-ID app // for the IP to be returned the service provider (SK) must switch on this option .withShareMdClientIpAddress(true) @@ -247,9 +227,10 @@ public void documentAuthenticatingWithSemanticsIdentifier() { /* - Note that verificationCode should be displayed by the web service, so the person signing through the Smart-ID mobile app can verify if the verification code displayed on the phone matches with the one shown on the web page. + Note that verificationCode should be displayed by the web service, so the person signing through the Smart-ID mobile app can verify + if the verification code displayed on the phone matches with the one shown on the web page. Leave a few seconds for the verification code to be displayed for users using the web service with their mobile device. - Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed. + Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed). ### Authenticating with document number @@ -297,57 +278,55 @@ public void documentAuthenticatingWithDocumentNumber() { */ - @Test(expected = UnprocessableSmartIdResponseException.class) + @Test public void documentAuthValidation() { - - // init Authentication response validator with trusted certificates loaded from within library - // as an alternative you can pass trusted certificates array as parameter to constructor - AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); - - // throws SmartIdResponseValidationException if validation doesn't pass - AuthenticationIdentity authIdentity = authenticationResponseValidator.validate(authenticationResponse); - - String givenName = authIdentity.getGivenName(); // e.g. Mari-Liis" - String surname = authIdentity.getSurname(); // e.g. "Männik" - String identityCode = authIdentity.getIdentityNumber(); // e.g. "47101010033" - String country = authIdentity.getCountry(); // e.g. "EE", "LV", "LT" - Optional birthDate = authIdentity.getDateOfBirth(); // see next paragraph - - - - /** - * ### Extracting date-of-birth - * Since all Estonian and Lithuanian national identity numbers contain date-of-birth - * this function always returns a correct value for them. - * - * For persons with Latvian national identity number the date-of-birth is parsed - * from a separate field but for some old Smart-id accounts the value might be missing. - * - * More info about the availability of the separate field in certificates: - * https://github.com/SK-EID/smart-id-documentation/wiki/FAQ#where-can-i-find-users-date-of-birth - */ - - Optional dateOfBirth = authIdentity.getDateOfBirth(); - - /** - One can also only fetch the signing certificate of a person - and then construct authentication identity from that - and extract the date-of-birth from there. - */ - - // skip these lines in readme.md - String certificate = "MIIIojCCBoqgAwIBAgIQJ5zu8nauSO5hSFPXGPNAtzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIwMDkyNjQ3WhcNMjQwOTIwMDkyNjQ3WjBlMQswCQYDVQQGEwJFRTEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEaMBgGA1UEBRMRUE5PRUUtMzk5MTIzMTk5OTcwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCI0y7aO3TlSbLgVRCGYmWZsiSg5U9ZIFjIBxQL9j6kYGUJZ+bGtyEmxXBj7KleqbueTqeZEEfzSPhtHuyPWuT4r7KfPl427/oKUpWcIrHWbLzLDFVAj4k9U2zN4vAAviTcVd6Qp/7ADsQgMAJFOktCfmLA82MHgWEh2E9jIL15I0HDbi5fuhWMv6FpUWJ/b4dZAzZjGvx9FMmoMw8OzHFc8JjfvsfaZ3DOlR/hGikFgeexEHt96mkmsnHO2vge/EHaggksIQg6OWubNodS+LN0MVvQCvNTFmBMyiHelSEiL/zDVxFoVQUc4WJmn+8i6nhTUq8C6uO+LvngIN22dUEfRn0+v2A9Yo/cuevPgMSFGFmJZL3sY1WCjdGPeku7uBq7S2H8nd37VhkPrKhfDUgMs1PP7aK3ESfNgW9gL/nlfYaWv/jMOaewEylQM+LUPJvVlpfAPRt4wOt6ZcJcS3t+NwQmGprtjtl8iWeQe3bfq35uVvvqBL/aA/CswhugXwLADKGYWhQa408FN4NRCuUFAVzi2foWjOP8MVE+ayR527+PcKykVBKn9JoNaPje7nigSoJLzXqRaz47QE2u8jFHEhVjwMwAwVQenaqQvEU0eWKdstIwoa9xOPNFMxFXkFrsuuyt22hIeRLN/nrxTMQnbwvmH7eQlM2bR6mA8ik5BJu4fzvsQsExsSxcX3WBfZc56/J1zizWoFMJ8+LOyqlZ6gPhVDzaFtEDOpT1C8m3GucpZQxSP0iJRr4XMYXKU8v3SDByYyCM9K1S/m9tZUOpjsHBX5xDrUXKdRXfrtk7qQJGngfEjSaQ12nweQgDIEpuIHoJ6m9yrOOMQa1CBJQGytHKBeXOB/nqF5IxzI5RTtrzEFLiqKqB+iFnPkA5PMsSCOGgAqGxg+of5eQtxIU7xgEeft7JxPnoDly5ohcnvip8/yAEptDgwJQybbEsbM4a+qjGkMz1O7ZrhptJR3VpppV7IIaLu/kxru7akHMuNXabYF+Sv3OzxhbRgTePT18CAwEAAaOCAkkwggJFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFPw86wO2tJOrY1RPmQeyY9TfaAf8MIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS0zOTkxMjMxOTk5Ny1BQUFBLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTAzMDMwMzEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBACQZH/fgKOUowei48VVlXJWLfxvyXTYKsp7SnS/VwtOj+y7IOQkTa+ZbHM27A5bhd+Bz1iruI5TSb3R2ZLF9U4KNXHbywaa7cAEimzXEMozeDvNdTkpawzTnCVih44iLCYdZ0GGRi6Wn6/Ue6EltN3hIucYPuzAO9dhwFrVSuTyaNSVKSi6TW/1jONNCX4+/XktcArArnarH5l+rfPQgecXYFvZ5xwywvFLrKXG1qUBtgH+3OrSsY4OtLiE56iCwMWGk/zpKa2ZSGPol8WmJIrHMEVR1jxUTMaEJLAEpiXbA2LH7+Js7/JPtbhbsyQGDjib4nNlle/ai29tKvX5cyccw1tCi7/KzcqwMI+Wy6fi6fVjdKFqI/bl3ouO7kqUO7STI+9xN6usMw+3Kb08FvX1ak8pDfiYod3iJ7Ky9+G8gLBxjApWB3ZfHn4aMz5SdaJBiuZvjk5kDbDk47wK/DuN+QkmXDWhftUsRbyNNHGT0M+qgbMzQ6b9OB6uZ957SfoB96vKUIN0oZ1ZSHpjMSqqlEv6wZO8+bmU6Bk3VqPDgBWvuJeztTdz+ylXhwx5TtClCSv0mw6bEcHJsOlgRyGu2XtGD0ILtfypfZNTzVtP9kqiKIXA+TkKtqfyR6ifry3kddJuqQ/swrpFb+/msYh367B1Rxca6ucgtfo2hKPQL"; - X509Certificate x509Certificate = CertificateParser.parseX509Certificate(certificate); - // skip previous 2 lines from readme.md - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(x509Certificate); - Optional signersCertificate = identity.getDateOfBirth(); - - assertThat(signersCertificate, CoreMatchers.is(LocalDate.of(1903,3,3))); - - // skip that: - - + assertThrows(UnprocessableSmartIdResponseException.class, () -> { + // init Authentication response validator with trusted certificates loaded from within library + // as an alternative you can pass trusted certificates array as parameter to constructor + AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); + + // throws SmartIdResponseValidationException if validation doesn't pass + AuthenticationIdentity authIdentity = authenticationResponseValidator.validate(authenticationResponse); + + String givenName = authIdentity.getGivenName(); // e.g. Mari-Liis" + String surname = authIdentity.getSurname(); // e.g. "Männik" + String identityCode = authIdentity.getIdentityNumber(); // e.g. "47101010033" + String country = authIdentity.getCountry(); // e.g. "EE", "LV", "LT" + Optional birthDate = authIdentity.getDateOfBirth(); // see next paragraph + + + /** + * ### Extracting date-of-birth + * Since all Estonian and Lithuanian national identity numbers contain date-of-birth + * this function always returns a correct value for them. + *

+ * For persons with Latvian national identity number the date-of-birth is parsed + * from a separate field but for some old Smart-id accounts the value might be missing. + *

+ * More info about the availability of the separate field in certificates: + * https://github.com/SK-EID/smart-id-documentation/wiki/FAQ#where-can-i-find-users-date-of-birth + */ + + Optional dateOfBirth = authIdentity.getDateOfBirth(); + + /** + One can also only fetch the signing certificate of a person + and then construct authentication identity from that + and extract the date-of-birth from there. + */ + + // skip these lines in readme.md + String certificate = "MIIIojCCBoqgAwIBAgIQJ5zu8nauSO5hSFPXGPNAtzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIwMDkyNjQ3WhcNMjQwOTIwMDkyNjQ3WjBlMQswCQYDVQQGEwJFRTEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEaMBgGA1UEBRMRUE5PRUUtMzk5MTIzMTk5OTcwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCI0y7aO3TlSbLgVRCGYmWZsiSg5U9ZIFjIBxQL9j6kYGUJZ+bGtyEmxXBj7KleqbueTqeZEEfzSPhtHuyPWuT4r7KfPl427/oKUpWcIrHWbLzLDFVAj4k9U2zN4vAAviTcVd6Qp/7ADsQgMAJFOktCfmLA82MHgWEh2E9jIL15I0HDbi5fuhWMv6FpUWJ/b4dZAzZjGvx9FMmoMw8OzHFc8JjfvsfaZ3DOlR/hGikFgeexEHt96mkmsnHO2vge/EHaggksIQg6OWubNodS+LN0MVvQCvNTFmBMyiHelSEiL/zDVxFoVQUc4WJmn+8i6nhTUq8C6uO+LvngIN22dUEfRn0+v2A9Yo/cuevPgMSFGFmJZL3sY1WCjdGPeku7uBq7S2H8nd37VhkPrKhfDUgMs1PP7aK3ESfNgW9gL/nlfYaWv/jMOaewEylQM+LUPJvVlpfAPRt4wOt6ZcJcS3t+NwQmGprtjtl8iWeQe3bfq35uVvvqBL/aA/CswhugXwLADKGYWhQa408FN4NRCuUFAVzi2foWjOP8MVE+ayR527+PcKykVBKn9JoNaPje7nigSoJLzXqRaz47QE2u8jFHEhVjwMwAwVQenaqQvEU0eWKdstIwoa9xOPNFMxFXkFrsuuyt22hIeRLN/nrxTMQnbwvmH7eQlM2bR6mA8ik5BJu4fzvsQsExsSxcX3WBfZc56/J1zizWoFMJ8+LOyqlZ6gPhVDzaFtEDOpT1C8m3GucpZQxSP0iJRr4XMYXKU8v3SDByYyCM9K1S/m9tZUOpjsHBX5xDrUXKdRXfrtk7qQJGngfEjSaQ12nweQgDIEpuIHoJ6m9yrOOMQa1CBJQGytHKBeXOB/nqF5IxzI5RTtrzEFLiqKqB+iFnPkA5PMsSCOGgAqGxg+of5eQtxIU7xgEeft7JxPnoDly5ohcnvip8/yAEptDgwJQybbEsbM4a+qjGkMz1O7ZrhptJR3VpppV7IIaLu/kxru7akHMuNXabYF+Sv3OzxhbRgTePT18CAwEAAaOCAkkwggJFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFPw86wO2tJOrY1RPmQeyY9TfaAf8MIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS0zOTkxMjMxOTk5Ny1BQUFBLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTAzMDMwMzEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBACQZH/fgKOUowei48VVlXJWLfxvyXTYKsp7SnS/VwtOj+y7IOQkTa+ZbHM27A5bhd+Bz1iruI5TSb3R2ZLF9U4KNXHbywaa7cAEimzXEMozeDvNdTkpawzTnCVih44iLCYdZ0GGRi6Wn6/Ue6EltN3hIucYPuzAO9dhwFrVSuTyaNSVKSi6TW/1jONNCX4+/XktcArArnarH5l+rfPQgecXYFvZ5xwywvFLrKXG1qUBtgH+3OrSsY4OtLiE56iCwMWGk/zpKa2ZSGPol8WmJIrHMEVR1jxUTMaEJLAEpiXbA2LH7+Js7/JPtbhbsyQGDjib4nNlle/ai29tKvX5cyccw1tCi7/KzcqwMI+Wy6fi6fVjdKFqI/bl3ouO7kqUO7STI+9xN6usMw+3Kb08FvX1ak8pDfiYod3iJ7Ky9+G8gLBxjApWB3ZfHn4aMz5SdaJBiuZvjk5kDbDk47wK/DuN+QkmXDWhftUsRbyNNHGT0M+qgbMzQ6b9OB6uZ957SfoB96vKUIN0oZ1ZSHpjMSqqlEv6wZO8+bmU6Bk3VqPDgBWvuJeztTdz+ylXhwx5TtClCSv0mw6bEcHJsOlgRyGu2XtGD0ILtfypfZNTzVtP9kqiKIXA+TkKtqfyR6ifry3kddJuqQ/swrpFb+/msYh367B1Rxca6ucgtfo2hKPQL"; + X509Certificate x509Certificate = CertificateParser.parseX509Certificate(certificate); + // skip previous 2 lines from readme.md + + AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(x509Certificate); + Optional signersCertificate = identity.getDateOfBirth(); + + assertThat(signersCertificate, CoreMatchers.is(LocalDate.of(1903, 3, 3))); + + // skip that: + }); } @@ -380,7 +359,7 @@ public void documentObtainingUsersCertificate() { /* If needed you can use semantics identifier instead of document number to obtain signer's certificate. - This may trigger a notification to all of the user's devices if user has more than one device with Smart-ID + This may trigger a notification to all the user's devices if user has more than one device with Smart-ID (as each device has separate signing certificate). @@ -399,8 +378,6 @@ You need to use other utilities (like [DigiDoc4j](https://github.com/open-eid/di @Test public void documentCreatingSignature() { - - SignableHash hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); // calculate hash from the document you want to sign (i.e. use Digidoc4J or other libraries) @@ -414,7 +391,7 @@ public void documentCreatingSignature() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") // returned as authentication result .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(asList( @@ -439,7 +416,7 @@ public void documentCreatingSignature() { Available interactions: * `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. * `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. -* `confirmationMessage` with `displayText200`. First screen is for text only (max 200 chars) and has Confirm and Cancel buttons. Second screen is for PIN. +* `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. * `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. @@ -463,7 +440,7 @@ public void documentCreatingSignature() { public void documentInteractionOrderMostCommon() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(Collections.singletonList( @@ -489,17 +466,16 @@ public void documentInteractionOrderMostCommon() { public void documentInteractionOrderVerificationChoice() { try { SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Arrays.asList( - Interaction.verificationCodeChoice("My confirmation message that is no more than 60 chars"), - Interaction.displayTextAndPIN("My confirmation message that is no more than 60 chars") - )) - .sign(); - } - catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException) { + .createSignature() + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(Arrays.asList( + Interaction.verificationCodeChoice("My confirmation message that is no more than 60 chars"), + Interaction.displayTextAndPIN("My confirmation message that is no more than 60 chars") + )) + .sign(); + } catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException) { System.out.println("User selected wrong verification code from 3-code choice"); } } @@ -518,7 +494,7 @@ public void documentInteractionOrderVerificationChoice() { public void documentInteractionOrderConfirmationWithFallbackToPin() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") // .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(asList( @@ -529,8 +505,7 @@ public void documentInteractionOrderConfirmationWithFallbackToPin() { if (InteractionFlow.CONFIRMATION_MESSAGE.is(smartIdSignature.getInteractionFlowUsed())) { System.out.println("Smart-ID app was able to display full text to user"); - } - else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { + } else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { System.out.println("Smart-ID app displayed shorter text to user"); } @@ -562,15 +537,13 @@ public void documentInteractionOrder2() { if (InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { System.out.println("Smart-ID app was able to display full text on separate screen and verification code choice."); - } - else if (InteractionFlow.VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { + } else if (InteractionFlow.VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { System.out.println("Smart-ID app displayed shorter text together with verification choice."); - } - else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { + } else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { System.out.println("Smart-ID app displayed shorter text to user with PIN input."); } - } + /* ### Listing interactions with longer text without fallback @@ -582,24 +555,19 @@ else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteraction */ @Test public void documentInteractionOrderWithoutFallback() { - try { client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here.") - )) - .sign(); - } - catch (RequiredInteractionNotSupportedByAppException e) { + .createSignature() + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withSignableHash(hashToSign) + .withCertificateLevel("QUALIFIED") + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.confirmationMessage("Long text (up to 200 characters) goes here.") + )) + .sign(); + } catch (RequiredInteractionNotSupportedByAppException e) { System.out.println("User's Smart-ID app is not capable of displaying required interaction"); } - - - } @@ -617,7 +585,6 @@ Under the hood each operation (authentication, choosing certificate and signing) @Test public void documentClientTimeoutConfig() { - SmartIdClient client = new SmartIdClient(); // ... // sets the timeout for each session status poll @@ -630,13 +597,12 @@ public void documentClientTimeoutConfig() { As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. -Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: + Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: */ @Test public void documentClientConnectionTimeoutConfig() { - SmartIdClient client = new SmartIdClient(); // ... ClientConfig clientConfig = new ClientConfig(); @@ -653,7 +619,7 @@ public void documentClientConnectionTimeoutConfig() { */ @Test - public void documentApacheHttpCleint() { + public void documentApacheHttpClient() { SmartIdClient client = new SmartIdClient(); // ... ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); @@ -667,9 +633,8 @@ public void documentApacheHttpCleint() { client.setNetworkConnectionConfig(clientConfig); } - @Test - @Ignore("you need to run a proxy to run this test") + @Disabled("you need to run a proxy to run this test") public void document_setProxy_withJbossRestEasy() throws Exception { // in order to run this test you can set up a proxy server locally //docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta @@ -691,25 +656,21 @@ public void document_setProxy_withJbossRestEasy() throws Exception { // CODE EXAMPLE ENDS HERE - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); + AuthenticationSessionRequest request = createAuthenticationSessionRequest(); SmartIdConnector smartIdConnector = client.getSmartIdConnector(); AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), smartIdConnector); assertAuthenticationResponseCreated(sessionStatus); - - - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); } @Test - @Ignore("you need a running proxy server to run this test") + @Disabled("you need a running proxy server to run this test") public void document_setNetworkConnectionConfig_withJersey() throws Exception { // in order to run this test you first have to set up a proxy server locally //docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta @@ -729,21 +690,17 @@ public void document_setNetworkConnectionConfig_withJersey() throws Exception { // CODE EXAMPLE ENDS HERE - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); + AuthenticationSessionRequest request = createAuthenticationSessionRequest(); SmartIdConnector smartIdConnector = client.getSmartIdConnector(); AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(isEmptyOrNullString())); + assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), smartIdConnector); assertAuthenticationResponseCreated(sessionStatus); - - // this allows to switch off tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); - } } diff --git a/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java index b781ccbe..c725e64c 100644 --- a/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java @@ -26,86 +26,50 @@ * #L% */ -import ee.sk.smartid.*; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.NationalIdentityNumberUtil; -import org.apache.commons.codec.binary.Base64; -import org.hamcrest.CoreMatchers; -import org.junit.Before; -import org.junit.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.security.cert.CertificateEncodingException; import java.time.LocalDate; import java.util.Collections; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isEmptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeTrue; +import org.apache.commons.codec.binary.Base64; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.FileUtil; +import ee.sk.SmartIdDemoIntegrationTest; +import ee.sk.smartid.AuthenticationHash; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.SignableData; +import ee.sk.smartid.SmartIdAuthenticationResponse; +import ee.sk.smartid.SmartIdCertificate; +import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.SmartIdSignature; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +@SmartIdDemoIntegrationTest public class SmartIdIntegrationTest { private static final String HOST_URL = "https://sid.demo.sk.ee/smart-id-rp/v2/"; private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String DOCUMENT_NUMBER = "PNOLT-30303039914-MOCK-Q"; private static final String DATA_TO_SIGN = "Well hello there!"; private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; private static final String CERTIFICATE_LEVEL_ADVANCED = "ADVANCED"; + + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + private SmartIdClient client; - /** - * Allows switching off tests going against smart-id demo env. - * This is sometimes needed if the test data in smart-id is temporarily broken. - */ - public static final boolean TEST_AGAINST_SMART_ID_DEMO = true; - - public static final String DEMO_HOST_SSL_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIIGoDCCBYigAwIBAgIQBOJYR4uzB/mihrGnWl+QIjANBgkqhkiG9w0BAQsFADBP\n" - + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE\n" - + "aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMjA5MTYwMDAwMDBa\n" - + "Fw0yMzEwMTcyMzU5NTlaMFUxCzAJBgNVBAYTAkVFMRAwDgYDVQQHEwdUYWxsaW5u\n" - + "MRswGQYDVQQKExJTSyBJRCBTb2x1dGlvbnMgQVMxFzAVBgNVBAMTDnNpZC5kZW1v\n" - + "LnNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoDLLTK+NEKsB\n" - + "POdOEjAK7/A8JTmZXlRkjM1aX0pfH6BCIGn3ZJd9M6iSR+KKQEfT0cj7JWvfMjZT\n" - + "oVHxOPbUaIUTdu22akLDy0kuZN78/RdqHUPq9WTKZsG3r03bi6tGqFb2KfzhZ2Q9\n" - + "zfS8Yn5N0iPeMh48BsreEdumb4F97JSEzjzFdGBb5wED//pHUL2VRoX1hzKV/6D8\n" - + "/sWmbMdGTYcXds/JbOIFU6EgAO2ozJUQmTbR2XRJYawKYAm4CEyY49zzvOldjOUC\n" - + "VjbheCxPJB0OeqYmfxm6QNqEi33Jsof9Y8uRl/DrEGexApd0bQkcGoGyBB08MWyu\n" - + "xjjmjh6TSQIDAQABo4IDcDCCA2wwHwYDVR0jBBgwFoAUt2ui6qiqhIx56rTaD5iy\n" - + "xZV2ufQwHQYDVR0OBBYEFIrtybLjSa2jrMVWly+c7KCBvpifMBkGA1UdEQQSMBCC\n" - + "DnNpZC5kZW1vLnNrLmVlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF\n" - + "BQcDAQYIKwYBBQUHAwIwgY8GA1UdHwSBhzCBhDBAoD6gPIY6aHR0cDovL2NybDMu\n" - + "ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDBA\n" - + "oD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hB\n" - + "MjU2MjAyMENBMS00LmNybDA+BgNVHSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUF\n" - + "BwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwfwYIKwYBBQUHAQEEczBx\n" - + "MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wSQYIKwYBBQUH\n" - + "MAKGPWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNI\n" - + "QTI1NjIwMjBDQTEtMS5jcnQwCQYDVR0TBAIwADCCAYAGCisGAQQB1nkCBAIEggFw\n" - + "BIIBbAFqAHcA6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4AAAGDRaWg\n" - + "0AAABAMASDBGAiEA0YjYuhVcbwncKefVPz4d8IrAQQ6ahcw5mOFufHTwbV8CIQCk\n" - + "oYVmHeYe9C9WeHYT4sKozs3ubeNqxPDRjKKaCPhtzQB2ADXPGRu/sWxXvw+tTG1C\n" - + "y7u2JyAmUeo/4SrvqAPDO9ZMAAABg0WloQQAAAQDAEcwRQIhALhRwut2GdVSxBnG\n" - + "KJOvCyaCySEhF7CXkhJRYsaZhBADAiB2X85UxwB5030w+1pX0QxJ4Z3A2sLwrwYR\n" - + "9/+yt4NGLwB3ALc++yTfnE26dfI5xbpY9Gxd/ELPep81xJ4dCYEl7bSZAAABg0Wl\n" - + "oRUAAAQDAEgwRgIhAPFc0KtyRqpNV3muD5aCzgE0RuQxsz6KPYKX4I49hfZeAiEA\n" - + "yuqiqCAtBkt/G7Wq4SA+/4xDyRKwXo5Zu8QuGGx9taYwDQYJKoZIhvcNAQELBQAD\n" - + "ggEBADTzrIM6pAvIClyXTGtyceDKckkGENmFmDvwL6I0Tab/s8uLlREpDhRPQpFQ\n" - + "hsAjaxWrfUv25EdYelBvaiOrCUwI3W3zlLy4gcgagEyTJ71lz7cH0VwFWjTsfXXc\n" - + "osD5sXMfipvkgmX+XgYJjsDY/HDFQyZp7aoTVqAlOfqkfsHi1EGdd6AGKP0yHokU\n" - + "3sUH1X6kDQdSfu1iwRPCn1CGS6xU1VJ6mJDU8SioBQKBAQkCs5UVdjdH+o99xsND\n" - + "8kfVHlchc+SxsI5cYhc4gUjjtX/U3FDZcW1IfZDil9tQf9l6rU/ZXMIPHeQWTPAa\n" - + "nUMrQKgVkBFH6CVchyHXPejDNGA=\n" - + "-----END CERTIFICATE-----"; - - - @Before + @BeforeEach public void setUp() { client = new SmartIdClient(); client.setRelyingPartyUUID(RELYING_PARTY_UUID); @@ -114,7 +78,6 @@ public void setUp() { client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); // temporary solution to skip tests going against smart-id demo env - assumeTrue(TEST_AGAINST_SMART_ID_DEMO); } @Test @@ -123,14 +86,14 @@ public void getCertificate_bySemanticsIdentifier() throws CertificateEncodingExc .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LT, "30303039914")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LT, "50609019996")) .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .withNonce("012345678901234567890123456789") .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039914-MOCK-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-50609019996-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIojCCBoqgAwIBAgIQMIn1C1GQ0CxjhLpGUCvWOzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjIxMTI4MTM0MDIyWhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkxUMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk5MTQwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQChuGkmE7wK3W5yw8vESPgyHL/sAHyv+3xcrK2jUUrKHwodOn2wzCioRu26uiZixdpnQbdb4KyZBCdBAIGduo7NdsLpfmwAtyGqenJqsbBX5tpvA4Stwoh4+fK5M1tifMItArpahGc26N0zXijZiNnirwkLmPkRMcYlS1zUuJfLOpwgqca38k4nVkX/PVOmtNSwNCKW+PVOlD0iaePPAqbWqCvkuyvazhyDDzmWqhGsY23+6iJZ/cpKz4B4VzRlzTVUBsGT5PegdETIIHFpvEfN/HtMugrfrTOnkd/Ymk1WbAdsNNLYp3hIAWsdIzSU1VhrShRPtp/QCAvEmpiRnbCTGkyjErAqyscVj2wAWmOagquB1Hb5O4hQ7Ksxp37FHi0zGqzCcanhwWiItOdM7RDmtlG2nGj6T/8iyYIlPwkYFd7fW5ka3agPAZV1y8PuKNh32gcbgnNsYJcBusK5kSynOY/LaSebrmnSc0jkmG4S8odbsNRaVlJGp3QP1qNWBqqFX/jUxTdgA4AxDtKSOpsevhJp/4jhHlAmwQxwuNskpNx65JI6fIrA+IgLy9SUFBQoPsrfwDMwgmJW8Rpjlb4F6y7KVD7z8jyCnIbHK/rMR9w0R4doF2q5Oivf1X4EEqkq9da0uXCMB2BZMex7b4GHAeKS99LaO/A6XfTYhek5qmxzrIYMY/0I3/sieSzdvuaVY0YN4o71Zw70gNgp8xMH9Dze/Lk/2sQjysteNfPzk4rIfMvZrg7TnCDNdzAhgWQ0tDkRM80g+83H9xN+t6aJoXoKe7CVckkFVZxeTtzMAyxJltifIsGa38FdasjWexbYUCw57qRplZifpLPB6YJCOn2n4/qtOY6sA0hkf8t5zuUdI6DXCEKcLyRKX4l0yEdAWzB/0LTnzBcAwoQO9FrCowRBjmGavvOSwJbeolTfCQd1IdxZF5Nk35EQ6qEA2XwdnyfN6JbNdJ1MSXvyLJZiPyKfRcmh0asJzLHJA/CIpOMBupxW9aRG9cJcwpOzfr0CAwEAAaOCAkkwggJFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFEWgA59+SJ1W3kWYF3wqP8MQxocUMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC0zMDMwMzAzOTkxNC1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTAzMDMwMzEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBAEOyA9CFBa1mpmZbFOb0giIQE/VenBLd1oZBupVm7VcW+pjR51JF7NBY+fcDkhx0vUB3bWobo2ivlqcUH7OpeROzyVgZCMdL7ezLTx1qEDPO6IcsYU1jTEsaJhTplbtBVJ0I43SJlF/mSQ/ypK9zNy40E7JWY070ewypdI9AmiG7cjRfD5gNgBK00mllNhLPK53L4+NIrBv22pvm9v4C5xEFTjCiHgd3lWXFcDKaM206k5wUf1LrcGNRQb4yS4SbToiqSdAxGoFJ3wpxpdv96ujo0ylMch1lmf/yA1pCnxys+qMCoTToPF4vtjj/1vWg0csD3UrFuLwHwuweWsWSqJVXUb9LfpPgfM/lPdQO2hQ1cVpXDBVnLAXfGfFcSX1CFnHpT5BKqlhIPDFJSB34F4yjqCMosL4Rvm35bniv2WXkQ9Cfsx1dueNB4CX7Wtc7wp5wRPiwAxAN9fmRRlKCxny/1h3/wGwfTlTixZ8PpcvdgcDdQEsssL6CY+1WEp8EPUvJetT8qKnd8KtpudV2bCBj8Z8xlAQYknz4CN+LSGbnoUqmeRvkReviE3E9SMazgL4Dm8hQ5qQc9xmq6YJpCz589dNEm2Ljy8eXvZ8NRbx0Wua0puqTm9prSDL/817mgq475GagBP9bCimzdBtfYZU+oCkHhaIeiZsqtYCNkMHd")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIzzCCBregAwIBAgIQZ5j2PEu1zGFm2sQyec4uhTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1ODI2WhcNMjQxMTA1MDk1ODI2WjB1MQswCQYDVQQGEwJMVDEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGjAYBgNVBAUTEVBOT0xULTUwNjA5MDE5OTk2MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAhETp4Pf67Q+DMpJeXqF+HqAMMDMbLcnGCZW5z457FYIqg924MFH/QkRv6JQcZnG0X6QbhRZrHpAOCUdkLqpwr1fkv59P4tGS+gqXGl/CPqHUDjg/ue8H0NQdBI9E7YC/jdT3y+1vudL5GiRgfaUyVPLZrABLfQ9IKNrw83blLiPJscEmBckDKAqQej5J7G6qPZL2gDMEKMFx/uMsvUYlAXL7HSsHHr+Et/uQWezJzTAR6uf7MCseFmEF1pDKKdK4ZA1W70ygzVQxgl2BI61Qmwbrz6o9eowFui8x5YebiGpW75zQk+3LHcOi53Y3YA9mfmjMKjWi81JOxPi/wEUXRJxotZ3vun3A3J45K8D0BD10AjdyFPDs8YWhgfgTWnliGDJDrnG5pD2LNr0XKwYLbqHnDlAbhAxGC/M3RPZROLOtA4y5NRHIZd4URTDts2lWEu7CfxiAvbqUXUAE4SfYtKm9KQeWf1KE20Rz7wBUUgNio2xiGr/phgjOzDi8QIcsw/4DiomfVvU+3i851+2YPO+JSs0wUazY6vBjHAC80ti2T1U21ctU5Ch7ITkwX+/vie/HVVq/gGEkkIkoKFf/CXPipSC9Q+/BoFYmCWQhqHynnu7vtXKa8mRwnLKHoJThgo8s1vEQ6w9aNWlzSNUmKlLh2YKyq+0OG9+ZmWRjy09+rsITi0RtQKpHtTKPKa5S08pI+I269rC58lmiEpo0nlP/0q0mmrv9t3sIi6UtHpoGXB204FoNMWXTvfbbbo5J4pSEufkTf132R4HB2rqPVtrm/I2zLgEPCdxruFOWteedkqQb6vNdRYhmg6dJfdByqj8XanyrH9zO39L9SXEQfqp7x+TVf0kEx/0x1q9E71dFNyIMgxt/avdA/S9oPi7ZAeOiWGdSUsSJgVuZzLLfXzwTvJKGy3WN6W+efTNw2tVdk8N9VAtseZE8sGs/FGlS5p8kiWKN56j6ZiehXcuM6pbWYumkuel8fq+ucV5KAahq3Ym8OTEcl9mZJ8bYTs7fAgMBAAGjggJmMIICYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFGa4Jvp40NbeOGbzE+cDCuJfATqiMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC01MDYwOTAxOTk5Ni1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8yMDA2MDkwMTEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBADNS7S82pT9RC1NCfMetNc9wwCkV83SUFlyTrOfr/c8jhkhQKfWCYegwEsUs3K+rUznE51JtLWoCYC0ftkdoIcbdXsR5d6Lq0jEcPwCG0JfSWuS8QFIKDZ/2FXNSjKAYbtqRuKDX+C3fhUjC9OgChGAxvnbXyvzY4whhmwCyQ6y5BpscZXE8+kSndfJsOJZV2Agd9t9WOVNg61DS9NJJ0OkRHn91NOQY3SFn5rpqdd7oOnd5ooUqmBPlcu0VByXXffUI+hJbiLdM8YhdbfHrkdqa9DfhgbOEEsY1YDqFVKgELCL7ISJ+Bsud+dy3VplVxiTQd4xQSfgEnPayhv/f0c9I7kYSnAe9fhz+sMqIgg+XEvms671QwCr5hJVCUUV+biFcYAemKxBWKzmPHA/1x5c337qpgEBnT51mkk+tz7jGG6KxmXLFVhJHPVQ+lKoEiZuMwvcAW5IBjZs7+aJe7E156cIr30T0LCnf7VK6++vZ5juePJ8i854bJZ27Rpds0TB2+4CWLwqk18hNHD9uVY4y9huruN8ndUNxHQ6cPiUsXr4c8f6Yh7gagJGcsfYWBDMVsXG1Oo6K0D1v5TEaESXSj8/3pWKC5Wj2tVMXNEcTESlEZ+JiXnqPQplOQ/MrI+ctsLgha+XSlWzyYDnLWikZOfQ40AvyxUZMfYvtwtIu")); } @Test @@ -139,14 +102,14 @@ public void getCertificate_bySemanticsIdentifier_latvian() throws CertificateEnc .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030403-10075")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990")) .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .withNonce("012345678901234567890123456789") .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-030403-10075-ZH4M-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-010906-29990-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw==")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIII0DCCBrigAwIBAgIQPnChot3bJ/pm2sRs94tubjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1OTI0WhcNMjQxMTA1MDk1OTI0WjB2MQswCQYDVQQGEwJMVjEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGzAZBgNVBAUTElBOT0xWLTAxMDkwNi0yOTk5MDCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAaFa/V82Gld+21Smxj/CB2etFLNx4QtWyWtNNlErvVysSmwL9jwfBJKQB0IXWo4GhQh1eucQ0gUBpatGVoghf0BB4AZJKamUV46RkaS63phR1EqHlaEp4R11hEs9ll0i9+Km5SINdBU9jqphGtyWJ/1iza9XEjWUJXS71slENdSlQjQ93LggYqxPtSXZ56KSfNWo5Uz8YBNo2aRhnp6h48HQrYk5WuoFW/uUm+Cf+4bO0iZzXGLIHZUTZKFJ2johcJdmfuwtApwzIS92U60gmQxPDG9yJrix6XHhfxYf1JTc4uMmYmTWvkUvV4qhUKU5F1jak1+3Le/+n9IVWN43qNTEFQJ8xINlfHsnlJ+5PNNR1HLLTmv2VQ3IFRB+EWFY6+w6OQgfHxyNDZnx/qqfvyVBllQ65p14VHsMv7uS9htBTkaDIT6995QVVdtFfORznNWKUVz/uPG2KRsQQxrFaOYCtJJ7bX2POMUquaMTv197pi1OcuHVyO4OZl9k/owVJeg0UKq/LCSaxT0F4zBr2twss3YX+5SgG2TyYANn4wU34rDzuPv52/yY0FH1IqWo2eglNsZSzxDhTjpQH3Gi4ogY7PkSg+yd1J3j9vSpBVsFV9FjRZ4xEV5CXMZCKHTWHP8aMIkpz1T6kY6gTV3aIEccFRvGWRzp6BQCRYGVwmtVv3UiLXqYiSHPgWyZwbyVQQu0nOPotNMpBo8XdNgYUneOVApa/zJokPXK8L1SiBlYEJvBfDwajS9VWvvpDznzqoo64np1D2YDcF6Bs7uZpg07oRbj62pXI1oG63weAgGw0cp266Mvkv2eIXgUDgh2u4LzGG71X0Nd/BJBeARFO7IWI1X63UT9hNOb8epRnCD3r69DSXejRnp4mdb35bNgDM5+V8acijVa55nq46cAk7BvMdkBW9DsJfoQ1msPeEf1lRuhulXyOHwg5BSAxc26+HTTj8rTKXT8lNufxsd2RkamlzLVVGm1+DnBBCTPf4JzjqE0+QMXuql0UEaCRJA9PAgMBAAGjggJnMIICYzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFKlFcvS8Z6fL+4bcIKckvaEM1ldqMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMTA5MDYtMjk5OTAtTU9DSy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwNjA5MDExMjAwMDBaMA0GCSqGSIb3DQEBCwUAA4ICAQDXIkSD7h52Qu5bPj1D/iY91Jlw6EVfhEnVuq4guT6J3jzE4fcYDNehI6P6Yted7HKCZEXW6kvvqxWx76GV7JtfrBZzu+Ru2gud1+wwVfgAkfyPkquklop4/flpDBz3bQCVkAbmdNLa+x939tGzlPIyL68JDEvHtxDLUa1mrAY8c2TNxcBAcUSukzE9vBfvjiuDoCsRylZ0DuEnG7qQ7qn+LGDFtWBiZ120V8ZLQpNRUkhkthYwm9aAt6j3l/KzawOB59rj7eJ1CUx7yfdpmB1M4BwAI1JOh0PMcpQ/gUnKB6CbU6SjTnexBTIGllht6WyZhyfKTs82useOorwn6PREVwhftIqkO/LYZD2dgzyqlNsEqglYU6oMUYKf4SbVhUZtYBuq9wTvvgsIGj9XYr8/9ZktnsWOWT+CEbmsdGncyJo1ubLSF4f5/NllZwSUdNqboM3rquW/IlJEJrjrOiepEdMYEEoz+zL93/RzZe0xGWitteYlfe8jXJilh9cJHGH6I+CM/s/lkgorRdKP80MotSRqBsztaeNLHq6r6Bls9P0G1PnEIzMwAwExJ3pe4NWjTfJud8coMXgeUhtxr9zqUr+hpXg5WHGCHxJ0qoR9x/YUk8E6szG5ccykk2Eu9tmPVoAaSAPPolQ10c2dLMvPtK8bDJr1dB8ht5E1zjVUqw==")); } @Test @@ -166,7 +129,7 @@ public void getCertificate_bySemanticsIdentifier_dateOfBirthParsedFromFieldInCer AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); assertThat(identity.getDateOfBirth().isPresent(), CoreMatchers.is(true)); - assertThat(identity.getDateOfBirth().get(), CoreMatchers.is(LocalDate.of(1999,12,31))); + assertThat(identity.getDateOfBirth().get(), CoreMatchers.is(LocalDate.of(1999, 12, 31))); } @Test @@ -181,13 +144,12 @@ public void getCertificate_EstonianByDocumentNumber_dateOfBirthParsedFromFieldIn .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-40404049996-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIoTCCBomgAwIBAgIQDnRWtLc1cm9jj2SA/ncFwzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjIxMjA2MTU0OTE5WhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkVFMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PRUUtNDA0MDQwNDk5OTYwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAHTW5RQN6eA/Iu51xFsFGJKyepBpovEzZ33XfvzJUbuNlsaQC/gEGZqkSG1NqcLx00AJXyxWiWXfwv5PGYYZoS4MVLFacUT/WkiI/cth6PevslhDVYxITooCYMhirmimKHvPd01XVzbGpvO498zW3qetLsv/FZcQyNV0Xh4JTVPEk05j6nQSZNh5dHSBzvLe41fzKPCw+N5KV3Szr3+Ov0i00jNbdV5kHgqSCvbr46iWrnew8MTO+Se6O4LatlZkAocwIQgpuYmvGL/ThhUHws4uVyKFHpdFsxdBA3BD4PpsXp3g4we3FNl2ZCj9W/o25jY3kryHcGZimE2iYa/139kpu+RggXZDQlQ+R6/p6ClM2W53hAtcr0HnZ+VEhMZ88MQTjvgqntyrMVbFqYrkpmlC5CPYhO5UDrUS6VFnv46iKP69QddWSkFQMUvjg7YDCGwFWtagYhRLK2hjTc3bF6CAV436SnDasY67RIFJrIrYnRbj0lv8SPph6nv/+khXwYp/DeF9xriuy69tPtoFlA3LxCeqPMMrUNgY3o/GcNqVh0TrUB0671DR9jmTrjl1dWfie6xdyO255MHWptBO1wys85LKNuy822DS0tdQLOZHsGXSNYCJUn0//9eeAMApX1a720G/C6qwyRf/wX1N1qhPJgMpTCFaWxfgmjFjYPnw7JjP+cCqZyIIH4+PPirLu1awVtcuPtTEHDEkUWnELKouXSltw8OpcblIs8ocVdfSy0Mil+09yz1fawi2zgulfLOj8I/liJo8c9KFvwOotFYRf2qVV8VuLM4OS1ucSLIH+fp2PtnyjyZOy1+2J0KlrxHRrTTejLRS/i4fkq+VWg2hIoAsYgpwgRNPqN7jvdaguaQcqyc9E8ht+w9pWep/SexC9bCKaDp8GUHu9ft9emoJQOOLB4RtI+O6V4arC8T3UbelL9u4zodKpUJiC2GTl8U6IrKjMSYqNObCbRM+fwF83/VP6WEK71EN3S9kFWRnGYE/bamIEaIBte3bc9cuIQIDAQABo4ICSTCCAkUwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwXQYDVR0gBFYwVDBHBgorBgEEAc4fAxECMDkwNwYIKwYBBQUHAgEWK2h0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABAjAdBgNVHQ4EFgQUaiwzCeEb6XKZ5WlgUMZj5/7264wwga4GCCsGAQUFBwEDBIGhMIGeMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfAYIKwYBBQUHAQEEcDBuMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBBBggrBgEFBQcwAoY1aHR0cDovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VJRC1TS18yMDE2LmRlci5jcnQwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNDA0MDQ5OTk2LU1PQ0stUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDQwNDA0MTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAFdJJqV/lvpVU489Ti0//cgynwgTE99wAVBpArgd8rD8apVMBoEn+Tu0Lez5YnfbK6+Dx1WvdM4t74xxkUlXkMIXLJI6iYM6mDiueDTvF94k51f1UWQo+/0GVO+dIDE1gmIm5K3eV/J7+/duSkrA72VHNJGCd8HVnj2UUOvo5VLBfQi7WjGjhff8LBXINUnBHIfs6CXrDJiLPwQQy/5pv03maJOG+isPT/IrhnkYBgOWDKaPCAkAvaGDaAPJGVNpu4QijuqKEzKrW9AGpmf1WxPhnp63zWOiEYuPhuqUnKH2IqG9gThi2l23zKU/7EbxOLd1vrElqAyHLvLS/PgSgiR/XxBUotxceeXYtnL20NxfzuYdEM1gz8UFyix4M5L905j/5Yuwksq/QN0c1A3gFQtHhtVrlSxzQpipd967HJezJxdsh6VlxuI0r6MSzcDOYVkOo3oE1sV/kyHtnhdWAVOh9u3EVtXBPyfWOMcPiloIDTJhbQ0pJFRLgEELSlYwObDzeqtRXMmtNpilK3feKu98PQekaQp1xv4dHyMIUsKLxNgyhGtV9o1mWoGpFaQImsF8jDeP2XckzmWh7s33SDm1/O4BgyyXbMNOa3HjP6l8LKb341M2lQAGs6JjelwIkOOUGYKr56SYshueeC92Xd/kOUY+pTCFQ87krYpBFETk=")); AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); - assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1904,4,4))); + assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1904, 4, 4))); } @@ -204,10 +166,10 @@ public void getCertificate_LithuanianByDocumentNumber_dateOfBirthParsedFromField assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039816-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIszCCBpugAwIBAgIQdDZ9/U3zfctjhLpHBt8J/TANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjIxMTI4MTM0MDIyWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkxUMR8wHQYDVQQDDBZURVNUTlVNQkVSLE1VTFRJUExFIE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMRQwEgYDVQQqDAtNVUxUSVBMRSBPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk4MTYwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAHArWoPq9Ups+75yOTOtOD9IxhlTe3PEV+aaLTJ/WUvEiz+8b1gu9x7eZUQ0eag0BDvgFP0YyQQ0W1ZTp4Orf26kfvytveuUOKhdMih7WKSj3Zih7leyNOc9I/Ub7cpJ2wTG3PX+bz4t1Bnto036tTPTdu0L2OO0ma2k+TcVfni0+WTY7o0/+mrQ8KzZZlGvQKIV8/AOzVICGi0W8CKqAtQ0dxhJdKBlDCcExAtIW2gVcbj2IQYR/Gfv6kLNbkRG5ULSKOpmeXczKChW2eACOkwJUKeEb5yZVQOWpa8DbenqHoIXaIsXzJ8U9tG3WS8Kw8OzpTqnKi3CMaXgiTghRXKdEi4VExcqOSdbi9DEqeHZUiFA/hW/stGiiFIIIj+G1UUmqizWK8ZIosq7HRPJLcaJknFMfiwzPpZdo6Bgq9D5dy5s8x37aEVSS6mCYWQ2u+YVvRA8gr+975GWa4ADRzpVzrCiHhi9UVHLhNpEHXKpSk/mKk8kwXePk4lv8FKeaoeuM3qU/+f9i/LHJmkLn8ZzJtjQvE4NQ8/x75NtAqCh5lYscqwNsjKzCbGJ89Ps/KgM3bRttqDZ/UtTDaNJxXZu6BcLK3NcC/ZTK1q6jeRc+HFi5SU+gqxK7vF61zwwPmI2cCuSlb5IsCackN++UaSwcISPkHyTPUID/lxqqsxbjKyz0oGAz3v3Jcc/tYY0yXEIK10C8d7bA/UJ5simpxcE6AlTygDr+7DuPZah6nI7O5pAUAvcEqZaMrv93BXZgCIpVdlLDJECRJpTzS9ItMTolgmbyBHsyW+jfHkyMhCRgFYnamIw7ztm+f47Ounn9qgMTnJmmf6u06Z7ZW1jPosQ3xb4NnXJRa9hK9lagDSjtYJCKwl9QQzaK5k6Ayzn3wdlYxduhn74t0ZiDYJCWCWltyW271Tz8XY7wPWjtv99mH1s9YoZsMpSGAj+NJ7HMw9bR0tLBf+sZB4wzKxKAlR520NNn32Ii6k9mVATQiEPFJbj2mB68hCX7qEtr1Hy3QIDAQABo4ICSTCCAkUwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwXQYDVR0gBFYwVDBHBgorBgEEAc4fAxECMDkwNwYIKwYBBQUHAgEWK2h0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABAjAdBgNVHQ4EFgQUhsfLf+5RtuqAwh8WeFgFdtzszG0wga4GCCsGAQUFBwEDBIGhMIGeMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfAYIKwYBBQUHAQEEcDBuMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBBBggrBgEFBQcwAoY1aHR0cDovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VJRC1TS18yMDE2LmRlci5jcnQwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0xULTMwMzAzMDM5ODE2LU1PQ0stUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAJqfsUnX3GTpzZL6m9MiQQk8D0xgtAmH+GStiBgphXAMyw72k82EQ8UCmhxflJpjXS6DTrB65y1FP33oNAOS+Ijz2wFYdxXRJT7hRvqk1zpuQqDNrbcDqqOA8mIGZbb1+TN4m0QRQlgTSEwicLkx9hwHUUyZ4mEVS8WJyj/+lU+64msslbEsHSxh8HY3UwyAh4dqw6hhQ2bWNCW0k87JuFthTJvSohZm6JcOhsfgMt29dDzhNmxZtetGQmbTZFg46RT+f+Utn19TLQJObEFFxkJY2FYA1mVEkKalyXAYmzbPJfSFhkDTpKgBjJLw1Jn/72hqTC5CikZX+LHvUK+JaRYIhvAh9b3qdtHeJLp5V7tLXTOokbt9MRvfgZAoMsVstY2zFSHGnZlO+/uqA98jLBQ/01+kCaMJeQ9fepPQq7T+4RKZhcLdxCuaFYiKASh5TATJjM5+fOPy86aOVkadUPHQflK2Tihul5qQl9weB8+LhgEdrg5nt3y/29SU4qHZ1UTJQLcqtOfbUcUaE0rZx5g4c0t7caSatBtPTxBVGQZmoGveqEzYLGivuSEwQglHiY1Br5vyRkIec+/oEWPMmkoiWSGIJDjBMv5aOzM0NR0NUtNcmBcvylhQeAxmnGl8XS4AH0CH9ZfnIpuziHNl+KjUr1Kp+25Mq2fY2c9vbxwI=")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIiTCCBnGgAwIBAgIQc6PEd785oXRm/SM0Oc1W1zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTA0MDUyWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkxUMR8wHQYDVQQDDBZURVNUTlVNQkVSLE1VTFRJUExFIE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMRQwEgYDVQQqDAtNVUxUSVBMRSBPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk4MTYwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAGWxALQGbmmDLrkdOFl8z0cUsCrGooykaSPBGQ6UhpT3rJZv9OsepcBS4WkGrWDfF70tyRZZ0HHDIjmae9fQBZ9eABkweWLhW76QxV8sFzUXD3hm4HByYI5OZwyTXAgXg6ZJvLioXl/4L+SYwCJI6/sOBkNpzzwbuOKVe3zYB4QlMzfgzE7GqBDjM621dk0KqE8lTaKrAiIxK+x+zWfXiCzll8+Fmqwfd2bjD67fzg+qsqS3AuwI5MoS7myaEil6ZeZnlEO35oXU5kyYjx7auKgmSp908vXmvQCvDs93lYU2WqYG+QRfoSrjF78JYzSZqEgibkX+uqZzIIyHGe/JRe4P0A4qRItoZR6MAl8v3a3tKkWHqfavFhmvuTdRKSHpJR27J5D8uhI1sTFMx8p/W0nITH7xFK6KQRee3AJYuwi/VhS3h52bdbkMsVnGzFA0MRImqeC0qA2TbRwm3ZyzAMpHoI5ESgw22SzBK6/15kz/fqEuLpIRUfZZg54+Vj+cNSvaLVosHfdXsJB0yow3VfV+o7PkH+UYVBzd52H1WusmQC3AWBb1hurlFCvbZbNr6AD5RJ/NLPsTc7Gl1Mt7rtEyM4Ov8HyTa4lWNvUc4VPm3lQDOU46kCFLruUlFd3phP9eZx61AznjaNW/MQd0kTV8/juhgooEOcn/sm4pnPu6JEJBHgZX4vIZa3ekCL/q6VRXGvQK5yADUqNNHpmkVuPUqc1g0vUZjtxZ7eNxtnJfz6/NkZqRsY8ONxKOcf/w6zT+crPopFvdK+fep0PXzU/7cVz5oeEzf8CdlAEidRyWLs4ZfUOYMbyvXmeCnmwkH5CxhNy0F3kfmoAf/iBN6L/w1p5NHoa1nNmeBjp2vu5ADnMxPP/lxUz5ToxZb+mIOKGsdGXNVKFnPWCyI6d6FtvnMqDyfhqjPEPeGfAxfNU3KJFWn7EE9EIplTCB9Tm6wSezTUi7dAevWJb4+rAfwQ1fwXYwMq9ZrYV+uKqsDKnzp4q/8DY9SoLMP/7tIA4FKQIDAQABo4ICHzCCAhswCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwXQYDVR0gBFYwVDBHBgorBgEEAc4fAxECMDkwNwYIKwYBBQUHAgEWK2h0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABAjAdBgNVHQ4EFgQUyhobJizCQFZeBpnDkNJI0+Fka5kwga4GCCsGAQUFBwEDBIGhMIGeMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfAYIKwYBBQUHAQEEcDBuMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBBBggrBgEFBQcwAoY1aHR0cDovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VJRC1TS18yMDE2LmRlci5jcnQwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0xULTMwMzAzMDM5ODE2LU1PQ0stUTANBgkqhkiG9w0BAQsFAAOCAgEALLxANwHBOLkkQzeHzV8APzCEo5LLky5Ha6ovodZ3WDsbIblxjMoq/BXw6AIkUD8zoq1pXFTvTR3ggzW6N8YAkDs+wtgg49P6bp7j96Tb5kFeYdleuqcRD/3r5IThqIpMCjFmjQDP3547Tbf4ELIIR9yvglgTw5we6V4CfrJ0RqET+V+7j1VmH+zg/S1E9lyaUOWE/5u9njWryj/ftmSqCJDTvzLigj2gOjiTWagzdGzQoRYXwuc5wefD+t5cLWHSumPkQidVUcPp7BUK3gaIBuXHMvnPN9sCTrXIg/AWwAzMUJzU94EN2cWb+k/EHYHIVsQ7j1qHS8cxvB6j+i4F4GbmmJQeKYEMGI6MCiMwlqVnNMzECaG2DQo7Rg9qQDY+bAxQDqtHoIeUY1Fvuwl4mQtyfsmUP4x7+B5AlKWB0lFUZ3Txez+HhGglA9+PYt1lHfpRmQMZzbaUvt2RDwp9f27vBqME+YKm6MFcJjDt5h+HfMosIAkZQOZbXc7EsTvEqtCcFwi9THAAZqAoG4LWqkLHDshov0ME8OlBre0OenKdexEav/ECSNFmxQG6o4tNDYA4tBRPb9Z7bEE6CPb/iFm34eBqFXy4uRVZkt5DBjhCZHXr9DxCoKj7UykptrtpY28sw/J67YJK3ci6pQQwzRfMgXZCWnxOCLNMYk0G9EA=")); AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); - assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1903,3,3))); + assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1903, 3, 3))); } @Test @@ -216,25 +178,25 @@ public void getCertificateEE_byDocumentNumber() throws CertificateEncodingExcept .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber(DOCUMENT_NUMBER) + .withDocumentNumber("PNOEE-50609019996-MOCK-Q") .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039914-MOCK-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-50609019996-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIojCCBoqgAwIBAgIQMIn1C1GQ0CxjhLpGUCvWOzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjIxMTI4MTM0MDIyWhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkxUMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk5MTQwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQChuGkmE7wK3W5yw8vESPgyHL/sAHyv+3xcrK2jUUrKHwodOn2wzCioRu26uiZixdpnQbdb4KyZBCdBAIGduo7NdsLpfmwAtyGqenJqsbBX5tpvA4Stwoh4+fK5M1tifMItArpahGc26N0zXijZiNnirwkLmPkRMcYlS1zUuJfLOpwgqca38k4nVkX/PVOmtNSwNCKW+PVOlD0iaePPAqbWqCvkuyvazhyDDzmWqhGsY23+6iJZ/cpKz4B4VzRlzTVUBsGT5PegdETIIHFpvEfN/HtMugrfrTOnkd/Ymk1WbAdsNNLYp3hIAWsdIzSU1VhrShRPtp/QCAvEmpiRnbCTGkyjErAqyscVj2wAWmOagquB1Hb5O4hQ7Ksxp37FHi0zGqzCcanhwWiItOdM7RDmtlG2nGj6T/8iyYIlPwkYFd7fW5ka3agPAZV1y8PuKNh32gcbgnNsYJcBusK5kSynOY/LaSebrmnSc0jkmG4S8odbsNRaVlJGp3QP1qNWBqqFX/jUxTdgA4AxDtKSOpsevhJp/4jhHlAmwQxwuNskpNx65JI6fIrA+IgLy9SUFBQoPsrfwDMwgmJW8Rpjlb4F6y7KVD7z8jyCnIbHK/rMR9w0R4doF2q5Oivf1X4EEqkq9da0uXCMB2BZMex7b4GHAeKS99LaO/A6XfTYhek5qmxzrIYMY/0I3/sieSzdvuaVY0YN4o71Zw70gNgp8xMH9Dze/Lk/2sQjysteNfPzk4rIfMvZrg7TnCDNdzAhgWQ0tDkRM80g+83H9xN+t6aJoXoKe7CVckkFVZxeTtzMAyxJltifIsGa38FdasjWexbYUCw57qRplZifpLPB6YJCOn2n4/qtOY6sA0hkf8t5zuUdI6DXCEKcLyRKX4l0yEdAWzB/0LTnzBcAwoQO9FrCowRBjmGavvOSwJbeolTfCQd1IdxZF5Nk35EQ6qEA2XwdnyfN6JbNdJ1MSXvyLJZiPyKfRcmh0asJzLHJA/CIpOMBupxW9aRG9cJcwpOzfr0CAwEAAaOCAkkwggJFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFEWgA59+SJ1W3kWYF3wqP8MQxocUMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC0zMDMwMzAzOTkxNC1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTAzMDMwMzEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBAEOyA9CFBa1mpmZbFOb0giIQE/VenBLd1oZBupVm7VcW+pjR51JF7NBY+fcDkhx0vUB3bWobo2ivlqcUH7OpeROzyVgZCMdL7ezLTx1qEDPO6IcsYU1jTEsaJhTplbtBVJ0I43SJlF/mSQ/ypK9zNy40E7JWY070ewypdI9AmiG7cjRfD5gNgBK00mllNhLPK53L4+NIrBv22pvm9v4C5xEFTjCiHgd3lWXFcDKaM206k5wUf1LrcGNRQb4yS4SbToiqSdAxGoFJ3wpxpdv96ujo0ylMch1lmf/yA1pCnxys+qMCoTToPF4vtjj/1vWg0csD3UrFuLwHwuweWsWSqJVXUb9LfpPgfM/lPdQO2hQ1cVpXDBVnLAXfGfFcSX1CFnHpT5BKqlhIPDFJSB34F4yjqCMosL4Rvm35bniv2WXkQ9Cfsx1dueNB4CX7Wtc7wp5wRPiwAxAN9fmRRlKCxny/1h3/wGwfTlTixZ8PpcvdgcDdQEsssL6CY+1WEp8EPUvJetT8qKnd8KtpudV2bCBj8Z8xlAQYknz4CN+LSGbnoUqmeRvkReviE3E9SMazgL4Dm8hQ5qQc9xmq6YJpCz589dNEm2Ljy8eXvZ8NRbx0Wua0puqTm9prSDL/817mgq475GagBP9bCimzdBtfYZU+oCkHhaIeiZsqtYCNkMHd")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIzzCCBregAwIBAgIQeQ1J2QDJ3Jlm2sN+JMQbPTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1NTI1WhcNMjQxMTA1MDk1NTI1WjB1MQswCQYDVQQGEwJFRTEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGjAYBgNVBAUTEVBOT0VFLTUwNjA5MDE5OTk2MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEArMoqLywQl6M6o1LDFW4iC2BUkyZAC+jINmWSQ0rispoTEOslzGw/aTfao8Yn5/KHZWKqZiFC7sy5qTGFOKt8xlTB7HJvKE58XsteZi7lTkxpK0m3haZQeb3G6dROKmfwtd7CrvSz0CPWaUogkPUZoO96fPuSs/xcWb4lL/M7OK/t8tdSJ5h/devSmVbGuL+Sder3FuyvtEtT8R5JnjChbEp6d1B7bIfKpgw8bdTGPbQvv145t3eQnCo3lx8vcZjTgAkiFoH1HmwrxonLnA40+qUKztxrbQFTi32dvxKiUPatDeCAKHgl4OXDZaOUvHeBylbd8I6aQ5PFHsXgBd9jccAHwaXYDM4qvSggwrHJNDujPa+drpYHL0f6N8pd36MrGiSekPyDTcg9RuIetUD+UzLO3vFusbC/anBaWs7UYaK1iTNT9AqlEhcnrovWZIuZ7/f3KSrnBkvJ9IQmdDbvwWg6m3oj6EZR9rxa8B+x9YWEYitPXbgwsYj5lPxyzGDYCtuXg42Xs0YbVwyWTfJu3Jmm113xjHbKQYdZrgxhFldqjo2W8FdFiggi3VaPUQa39GbC6/nSj5VKkglbTiH6JwP7edQaJ+5VikT1lAXJUHUQ3XYFGC50lhUrVrcYjIUMODBqux84i445ypUYZ83HAnmxMvsgjSVGAWsfIRYAnjC0nuVjhQRhNbX99LJ/aTu10fx9wfjOwxeSn6WLwWxR3I106thh81EtmY+qwf7Irb70VkswSGCd9k/UwXbv8LPdO4NgvNmuayaN01P3BGzxTP4W2mr0ATp3z0dhT5vz1jUxvHpijErlJwpVv6aOEAY68LnUvfy8jpOpiWvhiJOSpEE0yhVIx+/qfV9c0i0nw/ermVYupTOn44XlvqmePf6G/YwASV6vpi5aGtFeDvCPmiyxWrue/UWmJPE5vdueOWmdVokst7RVCUSXFEOC13O/6XhyofdPk201gSfhEwdsB/VKREXGVCAfjs6bu8IF7KEiOE/eSYNfb6nHW0BJNIqHAgMBAAGjggJmMIICYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFF3jcJ30p6biEtl7V0Inb2c6XjZrMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS01MDYwOTAxOTk5Ni1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8yMDA2MDkwMTEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBAF8gPJ2SkrXxGjqOO536D81KPqAxWbHdcVCzzg5LIRgumGlbbb3OyGSpmRfO+lHNKsni9XJP4kVwn6/9w2rRBmEm/x8U0ZoelWD6SNTPWggb787B3bAxZtEOEBiEfiJc2iCj2ZGuaLrzcz/sjTYo/+11X9411YBvDgHOYferCV8ms1IUv1mhWeE5jEn3jjxz8h04W4A3fN/ydOdTryxBdOV7+giQNQe71tOx1GQpDg6IXA4Da6CPUJxadGcWylAnOQFV1lkKk67revwkF1Z/2yAnTDz+3bx3DjUIlZXKx9Qg8/AXkcu8+ONvQaDn+QLp1qlRtcTUDfFi0bHiKPpv0dMvvGfEPug2G5QbA0jiWwmaZGJfdxBaRirFVLVl4WEE1+Sp5J8cIqEBpfCeLVDcpB6z2T2PAywL932QSHQ3jd/gwuKyZ/4VYxplnL2LazNvEh/Cv8JcvHoxh14bRRWWikdHcgB6K1TJ1nvnQPWnOBVPHp+W+1JYh26eE50dOW7UmqUrNgBm2FVMg0c6nufLghIwqRSHvJ/bX0Ovqby4aKy0Es1sRJNkYcuRUNf6LCMS7uR3EO4zOoiAzpUA5IEM6UUXMG92qNaJNT1uY/ImuSafSuRTd82SiBvl4XazNSl5Hgo4qMwD1SNjw4AmoFFi5dns7LYIqitnhjcUOtlgazE2")); } @Test public void getCertificateAndSignHash_withValidRelayingPartyAndUser_successfulCertificateRequestAndDataSigning() { SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .fetch(); + .getCertificate() + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) + .fetch(); assertCertificateChosen(certificateResponse); @@ -242,16 +204,16 @@ public void getCertificateAndSignHash_withValidRelayingPartyAndUser_successfulCe SignableData dataToSign = new SignableData(DATA_TO_SIGN.getBytes()); SmartIdSignature signature = client - .createSignature() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber(documentNumber) - .withSignableData(dataToSign) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("012345678901234567890123456789012345678901234567890123456789")) - ) - .sign(); + .createSignature() + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withDocumentNumber(documentNumber) + .withSignableData(dataToSign) + .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) + .withAllowedInteractionsOrder( + Collections.singletonList(Interaction.displayTextAndPIN("012345678901234567890123456789012345678901234567890123456789")) + ) + .sign(); assertSignatureCreated(signature); } @@ -262,15 +224,15 @@ public void authenticate_withValidUserAndRelayingPartyAndHash_successfulAuthenti assertNotNull(authenticationHash.calculateVerificationCode()); SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber(DOCUMENT_NUMBER) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(true) - .authenticate(); + .createAuthentication() + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withDocumentNumber("PNOLT-40404049996-MOCK-Q") + .withAuthenticationHash(authenticationHash) + .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) + .withShareMdClientIpAddress(true) + .authenticate(); assertAuthenticationResponseCreated(authenticationResponse, authenticationHash.getHashInBase64()); @@ -279,7 +241,7 @@ public void authenticate_withValidUserAndRelayingPartyAndHash_successfulAuthenti assertThat(authenticationIdentity.getGivenName(), is("OK")); assertThat(authenticationIdentity.getSurname(), is("TESTNUMBER")); - assertThat(authenticationIdentity.getIdentityNumber(), is("30303039914")); + assertThat(authenticationIdentity.getIdentityNumber(), is("40404049996")); assertThat(authenticationIdentity.getCountry(), is("LT")); System.out.println("Device IP: " + authenticationResponse.getDeviceIpAddress()); @@ -287,20 +249,20 @@ public void authenticate_withValidUserAndRelayingPartyAndHash_successfulAuthenti private void assertSignatureCreated(SmartIdSignature signature) { assertNotNull(signature); - assertThat(signature.getValueInBase64(), not(isEmptyOrNullString())); + assertThat(signature.getValueInBase64(), not(emptyOrNullString())); } private void assertCertificateChosen(SmartIdCertificate certificateResponse) { assertNotNull(certificateResponse); - assertThat(certificateResponse.getDocumentNumber(), not(isEmptyOrNullString())); + assertThat(certificateResponse.getDocumentNumber(), not(emptyOrNullString())); assertNotNull(certificateResponse.getCertificate()); } private void assertAuthenticationResponseCreated(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) { assertNotNull(authenticationResponse); - assertThat(authenticationResponse.getEndResult(), not(isEmptyOrNullString())); + assertThat(authenticationResponse.getEndResult(), not(emptyOrNullString())); assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); - assertThat(authenticationResponse.getSignatureValueInBase64(), not(isEmptyOrNullString())); + assertThat(authenticationResponse.getSignatureValueInBase64(), not(emptyOrNullString())); assertNotNull(authenticationResponse.getCertificate()); assertNotNull(authenticationResponse.getCertificateLevel()); } diff --git a/src/test/resources/demo_server_trusted_ssl_certs.jks b/src/test/resources/demo_server_trusted_ssl_certs.jks index baeb89854f2c677126f78ffefaab722c85d7a4d7..82d4918dbec9abd913b7c23bc83952880788a2ea 100644 GIT binary patch delta 1516 zcmaKrdoyB<`|Ji-ddH#6c^F7b!dEW2y6p$|(k`^WwCLjnx zBtbYws*~Yw)c1Tn-g&RQ$3BlL0Ibj@DNE{Cji;NBqip0ZcI4_cm$8m#jZg>#Q z%p9iy8o*gWRDB47k3?PHb!EN7V5#0aOh2CZI|Nb`rSl65R0%+>wTRxiayenbtVd z$@)9o)|v{)s&X__DgY+n^YNEl&8UECN;5IDFthLn-l7!QO`<+o3%;0k+x6@L`3jgZ z>aGZ6Ur#c0ED8g>*g1Fo12+e1iEVJs2X=mnN%;}SCs|gGJzckEbs`?q%$dcWZ`M!W z58ta4KT*5G=K5`C3VUd7UUi42?f83kdua2evu*>~6%OS-)B1D6VeCzJr>+L4x^-(0 z(Z~#}eMn)TG+L1IlAoK=!D!)a-#QATi!+WM4S5cG-)GYD7R7T5Ev1pI4sMyM@ttjv zPl(K79GWPZ#KUs-R} zpOKx?R+Bl;x_vl*@`ZfZIV7JKC8M!QP(#pUP$U1mx&HY<@)nUZg2E77l8$DBbTkua zNj_Uso1g|%(v*37)2@ZKKBf%vEYO#kfdSx(~<-y(195+!LTp^N#Zup2YQ(FIt(Tfr4U#}J)Fmj*#CAd(5kp!}+mR-5U1Itd&zI&+;Er0d;LLr;qX04E z#fVUM!_vNZ&KRGU>>dg^0SDwqh=1k6oce1P<*Xm;g7)06fz5|0WjfpBlSYpYKKuRe zrb8l$W^(^V!_-f{76tCMV@4Xar4Q_vkw#*(f~&cx;cyq+55+?%eq_br-3G}TL1B9W z=0D!c{6$&po>x?d8uu;Fp(I}ZO5XLE>vt2q)|0Lsz8YEqLrF79` zzn~BoI`!Q1#k&z)Z+{&YQNGYmK0QC{RMQy1xlkA%TjbkboL~?gI6iw>0)F02WmzTU zG|O9K4`b^(n!By0k7fvG>oaMZ^JMx#kFVgIe}NBoU*cgKAcd%Kx2^7GG@b2mVvN@u zeMj_-23@qtC6|HVo`rg`YC3b?J1s+97K!bg;QE}Y4SU8oVMaclpe|GQL$ zH=|E;UG_Q8qz0P~)^fdj&6A4~rDB*X?l&K6dh3yec1%hyo5^t?udyjBcGF=PsSLCp zDNC%Rc`+X|jCyQ`ON|Ojb=AtJ_~SRu2n$nghTdv<(AjX(K^)VRw%K(OsTMb`N_DKQ zh~*rR3y`zC^s3;cqUq{!-qie!10{xW!`Qg58W!hw?EP-|=3%%ksA4nNiN&fegj z-3YNeyHg}g|J3+#Es(U bBP!SY;r~}}*Z|q=8CF@}%HYWkR_^=f*3Xe0Ru3C7w-lMDuzgg_YDCD0ic2x-~@sf+%SR{*f4?@)CLJEhDe6@ z4FLxRpn@00FoGAs0s#Opf)~052`Yw2hW8Bt2LUiC1_~;MNQU^uHH0-T!5E(h|NCK0s{cUP=JCLfCYD(hqDGf&IK+0E9Q+AKF^ocKfD){K9%?! zLo&GCa}8q%tY{jmz^C(PI^Ocwr!cxB{VYd6@$M>{jZzxCX8{^Tb=b2o9{ZI)iH{POnndK^t$vj=gReuHs4v4mu= zU%(fhxIPX+w{RM?R^LF4oE@D;HZg+TiN=v@FV`(sws?59_e{60&tjI_^TF?p%kRzG zjxj}CTZ*-ga#*i|$NwgmIQztM7{5vP^}lta`E6Kisd5QOCZh6}rVb$U>j)^y0~RYF zdlb?93`~4h7DXP9-ubjJ4=6N+()d-!cKFSDk#mGDN%vE6PZ8+OzSO9L`DolxjG~{e zL1@|CuR=JKgzz)xB7tFb+B>Ub3 z8`U)AY#s@tm5@>0HTF8uL70FSF>UlUj8Zg>$DZUjKnO~#4>GM!`+~SF%6e5;c+Igp zcHg?lh6}+ru1Y}Pv1(LBa`82d9vTkeSucIQyh>kC7?Av+OfemrBqO!kK7lV#Ao`vK zBYyRHUQg*O8zO=r8i5<|G*D||L_^4FOvE4)<0?P4gMG!9N%XPV!{^j$wX0Xco~uyu zO*7u}Gn5!~!X4|^H{ve7#6e;xOYUTw`c*PKXQgCJP*ibgf@%*seGvB>9A;6u{m1#- zFZg@F2eGopcD#~I`s`&2HPaSk18Y~)BvS%QvFn7JybeqxJKlxx*VPmNh$VhwGvL>y zsj5Ko<{fA;D>-^Ybq)a5lUgXndqr+?EoV4lJ9*aUo&JwL)7K*xvbtVLGrK}4*>;Zo!BlYsEjCV!YE=Q7Ze z!cY}r3E;$ofS4TN048>3XB0=gfm63^<5iUEw97$SyB8PYs)^}x={i@#!Ob}{?Mz+v zkkc^w=4{9Y>@xDBROn{Q#|WKz^2ib+aK~}IZ{hs<+QSX21g@oK z)`C8@Xm2WO`}sy65ua)mzxr0(G^dbD+|@~+P%BPTBF6zF`i)sW4pH0Ar4C~Da6i^j zy*+=ts&Y!C&F(hHpuX6{neTSbT@QR=ptyhD=dtRWlH0(`pDc=i0!~Q$^mXp-9~OE5 z`zpUj$<2TqSn_L`xw3FRaLxW4rni!Jct#0((P@Q^2mCCF@aHu*z4g&A8pAjOo&c~6 zmn-@aS(Q>Sly|Ve6u;2UB)^jobB(lBoJxYqJXfW%tv||E3vkjQ+<#6wYE@MmQS+Jq z)XcaG5oPW5cZlL;%r)7exXpxJxQ9H0L*IcFl@V3F{#}y7?ShPEOjiCL;`&wc@xPDglZLs3vzQ`x-%jDy?qJ?_-DBRDGg}6&#=2pVKm#n40`$s

h%3a>Rj8&5F|O_J zlWgPgM0?w^2Mg%d7lDxz!lwpak!4U=bHS9Y-CDbW)0=9fD}+8gStcTMHT#9Ga~=4E z<;vjpiAcB1=?m--`9PmEod7weP+msfUtaGQ5WpTRC^?;{6Hb6}GY@&4z@MpbzvgZX zpHSOaZ5W1t-=B%q78 z>^(G|gtUVwMJY!Pa@trG_CspIkTP&zK2enGlv)Gn0L23(>vuc(zjt)4RlOXO4G8|I zk~wq1Scv~8$Hvi(s0tvXiLH6#Hor0C&~t*=G{oKQv9sP$>vkj8;( z@yU=yJe_!m5hoxsKqxd~*YCo&lC9P|aGnO*koI7Ku!gJoENG8G& zk`8tn-x?RZOF23rnIm=U%PG~YGzPkTJFMThN zsq0~hrjQmCF8qwlO;4u&po5Bo(*LCd?JCl&)H*d?41dk$qD~KmHP86GN^zL+uU$lZ z+?2HewBx%@#49wtKi-3Agy_@t$NI0uQ{zE|oVbj<%nz`ib1Iv6Aa1geRPP5ng7-YS zP?7u_EPB(KmDF8^Cc*$3#&$`dC?UEYb5b=HFWf!3$R&_mGFZtTU&jk zlJF$SMTcbtKfFBPT&-%vH;W#}6U|F`AzOObiXLG?yL)$|MvggEA*}FIU;xdWm@!Lr zA}hFFi>eZ$jaP+VsrMS~=sVva{aFR27P!VaH4_;Dwn|%`jIZ<`N}JrGQAA_(1EnF8 zmB3?dfn6^tq?#+Mr5AEh8%NGT0(0zsi#XHc7FTLz1}AbWgl7*h+G*Y@U={29-5Xp6 zAG4cH2!pHR-ndc+g6)2u& z2@5&AHyt|nI%BaTuIt;pGK?E3l|y2hF`!Lb2y67*;jDOZjS~h12%!mxy7Uot4<_tn zj5(ToQcT4mm1Mr_{}Q`ic!BJSjdV9SkUC!6oEx~_GznLOMcY=gSjdTnmwAeL;weY$ z(GsZ%mFeUrH)9Kr|JGy+H424$1b;p?-2_ZwrJ=ZPFXVuGkC%U&J$YUN2L(QMSF{RV zh4hS(l^Ij2F7+LSs4X<4iBxW8>0(v+(JXK#{DzUNMQMOS#5}>2@q}K5!thKY?^~Rs z1y}bwP^t07y0|xxs^;-MH*2?IZu}B)gKtPZ<-KkPnln@~%i5`5#Yw&_z7$z;9A&Eh zZnYx)~NdZ?1Ix>h8+i@ZpX^@^L$p2~2_Fm0zvxkF`)^VUgMnGgB zT4wYcd*_P5SvD}syo5YnDS^(O*~`dj!=8Dh9)XSS{YQ5YQ(Hk;xQ%~(j8}sQHJQNQeX0|w+WS{4 zMKY62r5Be#Dg*f6u9_9!+tr{6dP+OAlQB-xHW?9(3%DUHp&P<8mJs+laiCl#_op04 z!)Rtj=2dPP&awIz*R<4fI5skUt?vPyjyZ8T5p`yn+;&3i@@yj!v#FT+BCekR3+ zEet~m4gC((9Qdbbs{F)MllWnPrUeUMDE0Q5(^G6b$|Rbhw`$M!+c}Qv>+H0qExWip z)*at?jGnmdT66exfMy%ZU_nX7isNBnc=jiSW+E~zv=@>_F@SNfKIkfDJk~^>L1>h= z(tFI}mJ?!pIOv{|V7js0h@K8GIBjK4a|K>9Q3vGxbMbxnpkk4NozL6tT5b3W2b-v* zNjO$hH<2V4JF~7T=03eQ={5(fqhdcSuZD73ZskiKVQ;YXxmID+JlF10OAOT9^g@%ei zB5g<7Sy7yo*%mlpKbQ0nBMFy(k-t&%vF#C5yE_3Pis~A(6Jb;xv;0|pzdaQ?%Q=l!MS9zBf5M$YI^{>ie`xH^v=yjy zQ&a#}y5e2@D`uPP&1-EvE+!PC7Lm^#Hvg*Au=~q%wbjLEF}dn zv6y+_3cSmeLD=mXKDM(}Nx-Gng^WC7z!=@Qt7;x}+gJckn7Qs{r|q?cVjO1ghbQZaX;h z#iaHRR3{i0R94b;=^PhZ!Oa4AX3?X9ynJYmY0Mf3@N&9fu=`(d1>VOe z{^E1ZR));5O6MD`*{!b4wDR*N1%QB^2CU-DZ+dpouyUO7lmB;uA;{nJ7hym|1<@;$ z>&zOCE7hs&jEh~5jVG0}wOoEIfc`DXee*r(k1W?-n7?VE%8goc7q+pjUO(BasNXUl zH{nq(&@IvT&|U^1Om1~O*@NXb$n3)RcvsoG8A#}}SEg`)xECLQO>OD*YdW>Z(vOiT ztyOOdbw4aIV!M`VCkB6z2p2kN?-ZDU^(D(tCt;6B`<~RQ?|12=um+n)Nu*-GFWnEw z%iV;Ad|w{(#z2gVH0qs3YlNFc*KU0KypVn*+SHTG!fo8ozE4dDOquY00S6`O_B6M2 z5??-Rv$CP5i{$qk_fF1061@9ghl@ozM)dSkU%N;MZTbqjSeteY(idSxcZ57!A zpR#`0i5zXv-oZ29@{$iNL>PaCvF7_ddwYPGy=OaW!0L`K(}~%m5Sg<)PAo>UzXCEVF!b8C-3H|2Cl3EL+=E8l7!x?-ERqh8dOdi^LUJ$Qq57j!^W!I#>7h6} zX|6joXShY@c*xcKzgnl^oViXmFsH{FBPzf6bw?5Dl91-Qkc*%&Rv@P%1;ZsNTwu?R zp6o}~?1x8JK5UYAqj!@$x;gG@Lu0H#ox?+3#8U2p0L8pBJsw|tH#Nk|?I)Tx`FwY^ zVqL@LH{sj7DS(ahCOVWI5CJT|A~}hQ(AiHS@~q^y_QYs7lvtc!>Yo%-zwlO^gcqb{ z@NA^q^;b*`o9zLcIul?2U~+Cgnb+ZZjNF0QEim<|hKDyhiJ)mq0rmgF)Gt?iQJeL^ z{L3Dg_}l19mc&C4n8~XRr(0zx=->LsbdW9O;?Kg2b^@U&Wk!HTmqomIKl4Y|MM4ZY_ z^#6`sm#m{rqOq)qoyiXP@arXh)rDJqbrUBlg%n^(v_H$Ij?D%}Sm~BI$AY$N#M<|% zeJ}87j*(?`^c2dWVKTJI=1kAF09@`;3g8d&pa>D1U~l$C`>}PbmEdFAGNrKLv$YAAa*`G9e4lLO?PMo#)s z;q)1{m{vNRF<%gas*piV99%^$;Bz-4?ccU8lLVblR3)-m7eD#g)_9k!KaxZKh!?*d z(at)6TkvfN5F|yC>LRn(*+z=m zR(>kG+BBoyMg)NEiDrXcl(HCR_Cog9Au(zV(dKwmLDsj(UgqP>N>eF1%B+^atq+$L z{o9;-kG^a95B+vpr9!F)j-Ru$UV6jr06~wUKG}rsK*6$s{dH`$AkUh^A^#*gSPS}z zrwm^Rm7M7=YbBdqxEz8M#AF#$SEwmj%=WUI{}?=vnS*`z#V-OvEKd(Zvlhi{Iw-o2WJ_z>8M0pW%-X6BZIOIU!fB2(A_A0`YQN$>ia=GsraMWEae*S{mE<8Zw6exxE$F#yaiu0s{etpoIGYZ~y=R literal 4258 zcmV;T5MA#uf)Jhp0Ru3C5LX5XDuzgg_YDCD0ic2qNCbiqL@^WG0VsdArQiwF9)62G14pH6g z)oc1|X|6-Qa8CK!vuaai>1K&0kpxb!7SLOD5{MmtgIH}JucRh46l&_*<481Ekary$ zmIst9;O;AjiFL&8_fkvIu?yOnclz8C3Zj9Z=@%M=)E3uU;J{0Ryj<8B9zo@w z@(6*HA?2<4HS-GLi?7EsEEEZpsylKASq1WMUb1-aTV;n*Z2#|LJcYE+ z?mUkQXd-aNi5KL~JLAtPbDzHys>?1x_cyg}Ei67aj=_J1m&TcajRD-CXJQJEzZ@%l zKpB2CGC5hHb`2DlrZP7h(44i#$2MA>Q1u8q9!GP_Ab|E_oo7Tb$=ha|Fk}>vB7}x7 zcer*e2pm9n?Agq$)62$Xn`}AKeYNCy6+4BCjW!O19*}Ajm6aVg$>LJ`FxF2>lLul^~}Md9$6onYpy$T*4Qa z;asSGqkW9Hr<$I__O_Z?IIv(sht-)ULFEHjt@w z#EEqLu-9r~As^KTMKB<^u#n>ZTu(TNv>ieTjW*f~2<>&9?Hi5zO zoXJev=oa(1K70yg<@xTf^70)fjb|N_6xnQo1Hrt?O4lhc9IUJ$n|aCP8M#7b=xk7v&2dVG)P1;fy_1V$&lSYB#yvo>LDDMz)>aaH)=_*~qVuF8Uhv^p}fdSusQe_O+ zd#S5NcOcV6P2ik82)Ux1l+;;-P($0hqir&UcCKM%5cnV^gQLzDYl0eEz%%}tvA(L< zEbrj?yd<49;Ig5q{6WgwRsF=eY=IZcK2ZHZ9b$inSDUzOW({+()kE!Nr0@c5+@+j2 zxOP^m*HMO1HJJQl8UHhG&waXLvMxg$bc1J)nScAgmPdps-2eOK#HC9V+c5=R%h(D-Q`DJ;!Gb*)q(&8dpBb3IU%9*SoRt6<_K!Z#k})OW((|X zpV<=k1wz4OM8S;%YW6`@o5Gkx1y_if?#hnuV*frl#+TkB3c6b{haURlqDp6mi*7LR zU>4r#SH6h|u(6`OS*jGXZ*HmgW!Jxu+?H1nz+k#&YZw`YYPdS=IM!72j@;#sg@XB2 z(9jw<&bhU&K&P4ol^yJ&f*hqvgjUX)MLEKyW^C(Ls>o8^8+j)3*=2q0m|U|foo(-o z@+BZl3^2D(3;LbH(ZC>hWB$l~%l;$@MsU`q9gqJsR~cnmHAS4-qyGwu>1%MP32-b+ zJ22jylRqrPOOBh2(3RU4se-E}83%wWQQ*7%O}7I6zkGLHzolX7AQ>KYP)y}^UObY@ zC{Fj$D2*I}i^;267O`h)O#Q2fMc{IaFCIU1)IhQLUisL(4|1mdtF zw4FU6@yO%KzE`J2)o0JCrtOIlGWQXf)I<36Ccv!X| z0XZF<%>R!hL?FqSHeU!rKVz~p$kpJ-rl)sj$sw9h;|I9$&b5Q1NQ^;W}5Sgmb^c%be?*)Bpm(2`X7^|!LYN9@V=kUt!SB237M;MonywpUy%JS2$w9) z^9}cZC9@--8#+<1Z-P%coAENo~H;btBsKjl~tI(Q@sSy>TeZ)jR4k)0& zIl9^;I(uDQKG8|oMLtc4DkD-Kb1hIJqkQ3h_m%4;k9pF>+IqRBN zsFM4DDhZZ~FBj_2fj2QDe5V_k6&l;1A}{jDfU!%r=;-RXZ}u1LzRXrr<)XQ8uL-mJJ8x|!Lz@%{vl#aTTX0!gc7C}hhl_cJ+&9o~J6=R?(^2-J%~|W61UEDU519W^HkL21WbB->?y4)x?!TjTj~ko!3g zyRN}Ds(g~wG6(vp1B2h6@YVj}@SO#;SzB76uxG=42Ch54_fv7k1XQz8UOlf0Oa~*d z_MZO|fx(tRsTLzR=dry+ia?kHU%t+0K8CJK6!i2@<$`5Xhz$^_#tN6386}V$Q)kLz zWd0c_*SmCM>yxF)kZqI?fX2oGXBv# zr@k6v~u&pQA{xXrqU}ObE${o|*%GmuYW$`&hP2Dyyl%T}4|9AC{ta zFN=1Xk5E*DGb~z^eHYAAJP~+b-IfT@h1veIFL&)2Afdu1Cp~EA8Puvu0$-7Xh=RXd z>%KYROs79)-D|2F!mUUJECFCx!2p}YK<-R<$rg)`Fp8Z-p-RY*I$_M-NSH+QP0_-~ zd$cx&cMN|4?xbt}1DtKB_0C=l=8Qs1jiEm_)ZOVbcBtGHjhM7nA8>;=q1e2B7eRY? z*d78>+s@1^-cRHIKEV_v$t1+xnwoG|in`I1Wel&p!7~%wf_jo=Ok+XfE_^3j3{rUe-?*Of1s4?(<`w9U9$;52z~%JRBRk1NsR5_ zx#P0FlmSV5M1tb{^G*kR*Bjp&hjDu|gW#6D!P~&UT<#PpQ`o#n7Kd4fxch z1Y5$s5?MsY&$1o9sXPy_&;kfu_1r<%YSNq@e0_ejH527Tm#uX+W$8z8x=@kl? z)QPiw>oI395m`p2x1%7o5_Mat3W{RLTnswg=YxAIc?m??@t7x)z;za-w^8 znjxU<9V4D1m;U(;BO!W5d~CRr=X@-XxlC0Kwku~A2-#0k&e;tfylfYfd}2FsI+07A zZ(vIOVN3TyV1p`F`Iql#r+?l}0fF(>n+~c$^FPg&bkupVDpwC}#qK*GyeCd6(4321 z_YiK%fe;1lsUnbYyw~e2_wtqyv;#!Dh`0whiLCJAGx?5c5vc?kD;I5`=!sHZKplKQ z=rf0^w$eJJ6aW@@5GiEJuX%gh&rgQDy&X$}PLsRN^MNUSxrxQJ zadHY@CU3+kWZ1V|2p~A}3c*R2p2dV!JxmsM>%@gNoHq(SA0sb1OEBM;UwOH}Zl)mj z-Jca+sHqkKY$=-Yy*EMBZ&GDluH=|0%S})mc@9PtFQ{8&V?ognE>zZ;?Zyb!j8412 z%sH>)&<#J|n8uWC`09s7^q3V?;X112PTT$n!v(5VIA;pIF?Gp)H<$_gXy&W^6QOD$ z;uFHJW9Z7dL~_9Pk}wVDrJK;|tVK&^ym;MN;f{o%d)M(BS`?;ZY`ye!vI_#5Pe;Y- z5Bwv?t-VvO^eF(ZS|FXn!ALAutIB1uG5% z0vZJX1QhQd-fdvkxQ(g`-$`|nMe3=zof-rbMQSB3ukWCCeanxXO<~)(lZZL`0s{et Ep#R$&mjD0& diff --git a/src/test/resources/sid_demo_sk_ee.pem b/src/test/resources/sid_demo_sk_ee.pem new file mode 100644 index 00000000..7ae6d9c8 --- /dev/null +++ b/src/test/resources/sid_demo_sk_ee.pem @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIGxTCCBa2gAwIBAgIQB//0m9ljohCn8LB5KDcE1jANBgkqhkiG9w0BAQsFADBZ +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE +aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQx +MDAzMDAwMDAwWhcNMjUxMDE0MjM1OTU5WjBVMQswCQYDVQQGEwJFRTEQMA4GA1UE +BxMHVGFsbGlubjEbMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQQD +Ew5zaWQuZGVtby5zay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKAyy0yvjRCrATznThIwCu/wPCU5mV5UZIzNWl9KXx+gQiBp92SXfTOokkfiikBH +09HI+yVr3zI2U6FR8Tj21GiFE3bttmpCw8tJLmTe/P0Xah1D6vVkymbBt69N24ur +RqhW9in84WdkPc30vGJ+TdIj3jIePAbK3hHbpm+BfeyUhM48xXRgW+cBA//6R1C9 +lUaF9Ycylf+g/P7FpmzHRk2HF3bPyWziBVOhIADtqMyVEJk20dl0SWGsCmAJuAhM +mOPc87zpXYzlAlY24XgsTyQdDnqmJn8ZukDahIt9ybKH/WPLkZfw6xBnsQKXdG0J +HBqBsgQdPDFsrsY45o4ek0kCAwEAAaOCA4swggOHMB8GA1UdIwQYMBaAFHSFgMBm +x9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBSK7cmy40mto6zFVpcvnOyggb6YnzAZ +BgNVHREEEjAQgg5zaWQuZGVtby5zay5lZTA+BgNVHSAENzA1MDMGBmeBDAECAjAp +MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0f +BIGXMIGUMEigRqBEhkJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9j +cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAy +MENBMS0xLmNybDCBhwYIKwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC5kaWdpY2VydC5jb20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRp +Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0x +LmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdwAS +8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZJR+i+zAAAEAwBIMEYC +IQC7tPwb72Mur1ljtCP8g1/BkS6nJV0QeueW3eSa2L+PkwIhAPCJOyx++Vg5mE5D +6S0ctqbVRQsM5XGKYrBzAyzh0QHaAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65 +Ay/ZDowuebgAAAGSUfovdQAABAMARzBFAiEA6ifcmc/Si0vOqT4JTAMqervuE7Uz +iYGZIIZI09BYINICICeJuQZrqP7aHqn9+0iyvl5ptJl2cZ5YyqF3Km9f6vu4AHYA +5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6hzId/R43jlAAAAGSUfovjAAABAMARzBF +AiEAkdK3dAY6ABFtaE1bTjIlYAF5cFT8N2pvxL0mA79LlDwCIFGZJ3EYJfxVbj9m +S/8FynieG/02iMF6xzmmrU58La0pMA0GCSqGSIb3DQEBCwUAA4IBAQCnq3OnD4uw +uvt75qYIBgFNN+nIMslacl8iQYSOswr+K90QzL/yf+lLafDX0QMtDL5b2t1a834R +8efjlEuISfp+YjTdtnNV1jZ7nnkHcFMP1MGbv/JQigPO8AgL+oxGHiRCp6FNJTwt +FtvHkqd5rDJUU988LdND4aYtmKYmGKj06sSqhpl9xmbIxdXPvaJGoHC/gEpM8AKw +oL4afke2q3FpjQ1eDT+37pjsEjQi6nT0/cSNoyxy4QbqWBgGclmb9ZAfOFkaO5U3 +bhRopdPzRSrQROUF0ovPk4aC+b74KAV/oxtQjPTdpdxTVBwjfn2tpes5q+TZUGSZ +AyP23gCAvmuj +-----END CERTIFICATE----- diff --git a/src/test/resources/sid_live_sk_ee.pem b/src/test/resources/sid_live_sk_ee.pem new file mode 100644 index 00000000..8aabbe2c --- /dev/null +++ b/src/test/resources/sid_live_sk_ee.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN +MjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb +MBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h +cnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS +laHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj +jgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt +/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO +dRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan +gnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38 +yJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN +Rji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX +MBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW +MBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v +Y3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov +L2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3 +BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu +Y29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho +dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl +cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw +DAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K +cbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E +oZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd +0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g +gw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW +w+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu +zq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU +9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1 +fM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2 +FJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg +lWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t +Yv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu +ulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi +/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3 +dgE= +-----END CERTIFICATE----- From 850fc9166ea27da344211275dab182ccdb566ca0 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:56:13 +0300 Subject: [PATCH 02/57] SLIB-57 - added SmartIdClient and SmartIdConnector for v3 (#86) * SLIB-57 - added SmartIdClient and SmartIdConnector for v3 * SLIB-57 - refactored v2/v3 classes, added new demo cert * SLIB-57 - removed unecessary classes/methods --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 9 +- README.md | 34 ++- .../smartid/{ => v2}/AuthenticationHash.java | 2 +- .../{ => v2}/AuthenticationIdentity.java | 2 +- .../AuthenticationRequestBuilder.java | 30 ++- .../AuthenticationResponseValidator.java | 6 +- .../sk/smartid/{ => v2}/CertificateLevel.java | 2 +- .../smartid/{ => v2}/CertificateParser.java | 2 +- .../{ => v2}/CertificateRequestBuilder.java | 26 +- .../sk/smartid/{ => v2}/DigestCalculator.java | 2 +- .../java/ee/sk/smartid/{ => v2}/HashType.java | 2 +- .../ee/sk/smartid/{ => v2}/SignableData.java | 4 +- .../ee/sk/smartid/{ => v2}/SignableHash.java | 4 +- .../{ => v2}/SignatureRequestBuilder.java | 28 ++- .../SmartIdAuthenticationResponse.java | 2 +- .../smartid/{ => v2}/SmartIdCertificate.java | 2 +- .../ee/sk/smartid/{ => v2}/SmartIdClient.java | 18 +- .../{ => v2}/SmartIdRequestBuilder.java | 22 +- .../sk/smartid/{ => v2}/SmartIdSignature.java | 2 +- .../{ => v2}/VerificationCodeCalculator.java | 2 +- .../{ => v2}/rest/SessionStatusPoller.java | 5 +- .../{ => v2}/rest/SmartIdConnector.java | 11 +- .../{ => v2}/rest/SmartIdRestConnector.java | 15 +- .../dao/AuthenticationSessionRequest.java | 2 +- .../dao/AuthenticationSessionResponse.java | 2 +- .../smartid/{ => v2}/rest/dao/Capability.java | 2 +- .../rest/dao/CertificateChoiceResponse.java | 2 +- .../{ => v2}/rest/dao/CertificateRequest.java | 2 +- .../{ => v2}/rest/dao/Interaction.java | 10 +- .../{ => v2}/rest/dao/InteractionFlow.java | 2 +- .../{ => v2}/rest/dao/RequestProperties.java | 2 +- .../rest/dao/SemanticsIdentifier.java | 2 +- .../{ => v2}/rest/dao/SessionCertificate.java | 2 +- .../{ => v2}/rest/dao/SessionResult.java | 2 +- .../{ => v2}/rest/dao/SessionSignature.java | 2 +- .../{ => v2}/rest/dao/SessionStatus.java | 2 +- .../rest/dao/SessionStatusRequest.java | 2 +- .../rest/dao/SignatureSessionRequest.java | 2 +- .../rest/dao/SignatureSessionResponse.java | 2 +- .../util/CertificateAttributeUtil.java | 4 +- .../util/NationalIdentityNumberUtil.java | 4 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 235 ++++++++++++++++++ .../smartid/v3/rest/SessionStatusPoller.java | 51 ++++ .../sk/smartid/v3/rest/SmartIdConnector.java | 53 ++++ .../smartid/v3/rest/SmartIdRestConnector.java | 203 +++++++++++++++ .../sk/smartid/v3/rest/dao/Interaction.java | 132 ++++++++++ .../smartid/v3/rest/dao/InteractionFlow.java | 53 ++++ .../v3/rest/dao/RequestProperties.java | 52 ++++ .../v3/rest/dao/SemanticsIdentifier.java | 70 ++++++ .../sk/smartid/v3/rest/dao/SessionResult.java | 54 ++++ .../sk/smartid/v3/rest/dao/SessionStatus.java | 97 ++++++++ .../v3/rest/dao/SessionStatusRequest.java | 73 ++++++ src/test/java/ee/sk/smartid/DummyData.java | 65 ----- .../smartid/integration/ReadmeTest.java | 46 ++-- .../integration/SmartIdIntegrationTest.java | 26 +- .../sk/smartid/rest/SmartIdConnectorSpy.java | 107 -------- .../{ => v2}/AuthenticationIdentityTest.java | 4 +- .../AuthenticationRequestBuilderTest.java | 57 ++--- .../AuthenticationResponseValidatorTest.java | 3 +- .../{ => v2}/CertificateLevelTest.java | 4 +- .../{ => v2}/CertificateParserTest.java | 3 +- .../CertificateRequestBuilderTest.java | 48 ++-- .../sk/{ => smartid/v2}/CertificateUtil.java | 8 +- .../{ => v2}/ClientRequestHeaderFilter.java | 30 +-- .../{ => v2}/DigestCalculatorTest.java | 4 +- src/test/java/ee/sk/smartid/v2/DummyData.java | 65 +++++ ...ndpointSslVerificationIntegrationTest.java | 5 +- .../java/ee/sk/{ => smartid/v2}/FileUtil.java | 2 +- .../sk/smartid/{ => v2}/SignableDataTest.java | 5 +- .../sk/smartid/{ => v2}/SignableHashTest.java | 6 +- .../{ => v2}/SignatureRequestBuilderTest.java | 41 +-- .../SmartIdAuthenticationResponseTest.java | 2 +- .../smartid/{ => v2}/SmartIdClientTest.java | 232 +++++++++-------- .../v2}/SmartIdDemoCondition.java | 2 +- .../v2}/SmartIdDemoIntegrationTest.java | 2 +- .../{ => v2}/SmartIdRestServiceStubs.java | 2 +- .../{ => v2}/SmartIdSignatureTest.java | 3 +- .../VerificationCodeCalculatorTest.java | 6 +- .../rest/SessionStatusPollerTest.java | 24 +- .../smartid/v2/rest/SmartIdConnectorSpy.java | 116 +++++++++ .../rest/SmartIdRestConnectorTest.java | 166 +++++++------ .../rest/SmartIdRestIntegrationTest.java | 28 ++- .../rest/dao/SemanticsIdentifierTest.java | 4 +- .../rest/dao/SignatureSessionRequestTest.java | 2 +- .../util/CertificateAttributeUtilTest.java | 5 +- .../util/NationalIdentityNumberUtilTest.java | 9 +- .../demo_server_trusted_ssl_certs.jks | Bin 5685 -> 7006 bytes 87 files changed, 1838 insertions(+), 647 deletions(-) rename src/main/java/ee/sk/smartid/{ => v2}/AuthenticationHash.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/AuthenticationIdentity.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/AuthenticationRequestBuilder.java (94%) rename src/main/java/ee/sk/smartid/{ => v2}/AuthenticationResponseValidator.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/CertificateLevel.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/CertificateParser.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/CertificateRequestBuilder.java (94%) rename src/main/java/ee/sk/smartid/{ => v2}/DigestCalculator.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/HashType.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/SignableData.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/SignableHash.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/SignatureRequestBuilder.java (94%) rename src/main/java/ee/sk/smartid/{ => v2}/SmartIdAuthenticationResponse.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/SmartIdCertificate.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/SmartIdClient.java (93%) rename src/main/java/ee/sk/smartid/{ => v2}/SmartIdRequestBuilder.java (89%) rename src/main/java/ee/sk/smartid/{ => v2}/SmartIdSignature.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/VerificationCodeCalculator.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/SessionStatusPoller.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/SmartIdConnector.java (82%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/SmartIdRestConnector.java (95%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/AuthenticationSessionRequest.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/AuthenticationSessionResponse.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/Capability.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/CertificateChoiceResponse.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/CertificateRequest.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/Interaction.java (92%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/InteractionFlow.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/RequestProperties.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SemanticsIdentifier.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SessionCertificate.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SessionResult.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SessionSignature.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SessionStatus.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SessionStatusRequest.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SignatureSessionRequest.java (99%) rename src/main/java/ee/sk/smartid/{ => v2}/rest/dao/SignatureSessionResponse.java (97%) rename src/main/java/ee/sk/smartid/{ => v2}/util/CertificateAttributeUtil.java (98%) rename src/main/java/ee/sk/smartid/{ => v2}/util/NationalIdentityNumberUtil.java (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/SmartIdClient.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java delete mode 100644 src/test/java/ee/sk/smartid/DummyData.java rename src/test/java/ee/sk/{test => }/smartid/integration/ReadmeTest.java (96%) rename src/test/java/ee/sk/{test => }/smartid/integration/SmartIdIntegrationTest.java (98%) delete mode 100644 src/test/java/ee/sk/smartid/rest/SmartIdConnectorSpy.java rename src/test/java/ee/sk/smartid/{ => v2}/AuthenticationIdentityTest.java (97%) rename src/test/java/ee/sk/smartid/{ => v2}/AuthenticationRequestBuilderTest.java (92%) rename src/test/java/ee/sk/smartid/{ => v2}/AuthenticationResponseValidatorTest.java (99%) rename src/test/java/ee/sk/smartid/{ => v2}/CertificateLevelTest.java (97%) rename src/test/java/ee/sk/smartid/{ => v2}/CertificateParserTest.java (95%) rename src/test/java/ee/sk/smartid/{ => v2}/CertificateRequestBuilderTest.java (86%) rename src/test/java/ee/sk/{ => smartid/v2}/CertificateUtil.java (96%) rename src/test/java/ee/sk/smartid/{ => v2}/ClientRequestHeaderFilter.java (73%) rename src/test/java/ee/sk/smartid/{ => v2}/DigestCalculatorTest.java (96%) create mode 100644 src/test/java/ee/sk/smartid/v2/DummyData.java rename src/test/java/ee/sk/smartid/{ => v2}/EndpointSslVerificationIntegrationTest.java (99%) rename src/test/java/ee/sk/{ => smartid/v2}/FileUtil.java (98%) rename src/test/java/ee/sk/smartid/{ => v2}/SignableDataTest.java (97%) rename src/test/java/ee/sk/smartid/{ => v2}/SignableHashTest.java (93%) rename src/test/java/ee/sk/smartid/{ => v2}/SignatureRequestBuilderTest.java (93%) rename src/test/java/ee/sk/smartid/{ => v2}/SmartIdAuthenticationResponseTest.java (99%) rename src/test/java/ee/sk/smartid/{ => v2}/SmartIdClientTest.java (86%) rename src/test/java/ee/sk/{ => smartid/v2}/SmartIdDemoCondition.java (98%) rename src/test/java/ee/sk/{ => smartid/v2}/SmartIdDemoIntegrationTest.java (98%) rename src/test/java/ee/sk/smartid/{ => v2}/SmartIdRestServiceStubs.java (99%) rename src/test/java/ee/sk/smartid/{ => v2}/SmartIdSignatureTest.java (97%) rename src/test/java/ee/sk/smartid/{ => v2}/VerificationCodeCalculatorTest.java (93%) rename src/test/java/ee/sk/smartid/{ => v2}/rest/SessionStatusPollerTest.java (92%) create mode 100644 src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java rename src/test/java/ee/sk/smartid/{ => v2}/rest/SmartIdRestConnectorTest.java (85%) rename src/test/java/ee/sk/smartid/{ => v2}/rest/SmartIdRestIntegrationTest.java (94%) rename src/test/java/ee/sk/smartid/{ => v2}/rest/dao/SemanticsIdentifierTest.java (95%) rename src/test/java/ee/sk/smartid/{ => v2}/rest/dao/SignatureSessionRequestTest.java (97%) rename src/test/java/ee/sk/smartid/{ => v2}/util/CertificateAttributeUtilTest.java (98%) rename src/test/java/ee/sk/smartid/{ => v2}/util/NationalIdentityNumberUtilTest.java (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eea0cd4..f5bc4dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [3.0] - upcoming +## [3.0] - 2023-10-14 + +### Added +- Support for Smart-ID API v3.0 has been added under the ee.sk.smartid.v3 package. + ### Changed +- Existing code for Smart-ID API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. - Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` - Typo fixes, code cleanup and improvements - Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. @@ -56,7 +61,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java#:~:text=getDeviceIpAddress()) - [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java#:~:text=getDeviceIpAddress()) -- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) +- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) ## [2.1.4] - 2022-01-14 diff --git a/README.md b/README.md index f3062e85..61639c68 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ # Smart-ID Java client -This version of the library uses Smart-ID API v 2.0. - +This library now supports both Smart-ID API v2.0 and v3.0. # Table of contents @@ -50,6 +49,7 @@ This version of the library uses Smart-ID API v 2.0. ## Introduction The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. +This library now supports both Smart-ID API v2.0 and v3.0. The existing code for API v2.0 has been moved to the ee.sk.smartid.v2 package, and support for API v3.0 has been added in the ee.sk.smartid.v3 package. ## Features @@ -81,8 +81,13 @@ You can use the library as a Maven dependency from the [Maven Central](https://s Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) +In this version, the existing code has been moved into the ee.sk.smartid.v2 package for clarity. This is a breaking change for current users of the library. +To update your application: +Change your import statements from ee.sk.smartid.* to ee.sk.smartid.v2.* +Update any references to classes, methods, or packages accordingly. +Support for Smart-ID API v3.0 has been added in the ee.sk.smartid.v3 package. Documentation for v3.0 is currently limited as it is in the early stages of development. -# How to use it +# How to use API v2.0 ## Test accounts for testing @@ -115,7 +120,7 @@ The returned info can be retrieved using one of: * [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java) -> getDeviceIpAddress() * [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java) -> getDeviceIpAddress() -* [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java) -> getDeviceIpAddress() +* [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java) -> getDeviceIpAddress() ## Example of configuring the client @@ -636,3 +641,24 @@ you have two alternatives: client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); ``` +# How to use API v3.0 + +Support for Smart-ID API v3.0 has been added to the library. The code for v3.0 is located under the ee.sk.smartid.v3 package. +This version introduces new dynamic link and notification-based flows for both authentication and signing. + +To use the v3.0 API, import the relevant classes from the ee.sk.smartid.v3 package. +```java + import ee.sk.smartid.v3.SmartIdClient; + import ee.sk.smartid.v3.SmartIdConnector; +``` + +## Setting up SmartIdClient for v3.0 + +```java + import ee.sk.smartid.v3.SmartIdClient; + +var client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +``` \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/AuthenticationHash.java b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java similarity index 98% rename from src/main/java/ee/sk/smartid/AuthenticationHash.java rename to src/main/java/ee/sk/smartid/v2/AuthenticationHash.java index 6f3440f6..82a12930 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java similarity index 99% rename from src/main/java/ee/sk/smartid/AuthenticationIdentity.java rename to src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java index 5a8af3fd..13b844a4 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java similarity index 94% rename from src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java rename to src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java index dd6c8456..898b288c 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -33,9 +33,19 @@ import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.Capability; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.RequestProperties; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionCertificate; +import ee.sk.smartid.v2.rest.dao.SessionResult; +import ee.sk.smartid.v2.rest.dao.SessionSignature; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SmartIdConnector; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +62,7 @@ *

* Mandatory request parameters: *

    - *
  • Host url - can be set on the {@link ee.sk.smartid.SmartIdClient} level
  • + *
  • Host url - can be set on the {@link SmartIdClient} level
  • *
  • Relying party uuid - can either be set on the client or builder level
  • *
  • Relying party name - can either be set on the client or builder level
  • *
  • Either Document number or semantics identifier or private company identifier
  • @@ -84,9 +94,9 @@ public AuthenticationRequestBuilder(SmartIdConnector connector, SessionStatusPol * Sets the request's UUID of the relying party *

    * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} + * {@link SmartIdClient#setRelyingPartyUUID(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set the UUID every time when building a new request. * * @param relyingPartyUUID UUID of the relying party @@ -101,9 +111,9 @@ public AuthenticationRequestBuilder withRelyingPartyUUID(String relyingPartyUUID * Sets the request's name of the relying party *

    * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} + * {@link SmartIdClient#setRelyingPartyName(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set name every time when building a new request. * * @param relyingPartyName name of the relying party @@ -161,7 +171,7 @@ public AuthenticationRequestBuilder withSemanticsIdentifier(SemanticsIdentifier * which is essential for the authentication verification. * For security reasons the hash should be generated * randomly for every new request. It is recommended to use: - * {@link ee.sk.smartid.AuthenticationHash#generateRandomHash()} + * {@link AuthenticationHash#generateRandomHash()} * * @param authenticationHash hash used to sign for authentication * @return this builder diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java similarity index 99% rename from src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java rename to src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java index 3837dabf..e7ba7402 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -57,8 +57,8 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.NationalIdentityNumberUtil; +import ee.sk.smartid.v2.util.CertificateAttributeUtil; +import ee.sk.smartid.v2.util.NationalIdentityNumberUtil; import ee.sk.smartid.util.StringUtil; /** diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/v2/CertificateLevel.java similarity index 98% rename from src/main/java/ee/sk/smartid/CertificateLevel.java rename to src/main/java/ee/sk/smartid/v2/CertificateLevel.java index 31203b7b..c2804b92 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateLevel.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/CertificateParser.java b/src/main/java/ee/sk/smartid/v2/CertificateParser.java similarity index 98% rename from src/main/java/ee/sk/smartid/CertificateParser.java rename to src/main/java/ee/sk/smartid/v2/CertificateParser.java index 7902337f..e609fa3b 100644 --- a/src/main/java/ee/sk/smartid/CertificateParser.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateParser.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java similarity index 94% rename from src/main/java/ee/sk/smartid/CertificateRequestBuilder.java rename to src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java index d37294d3..a6122e05 100644 --- a/src/main/java/ee/sk/smartid/CertificateRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -33,9 +33,17 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.dao.Capability; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.RequestProperties; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionCertificate; +import ee.sk.smartid.v2.rest.dao.SessionResult; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SmartIdConnector; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +60,7 @@ *

    * Mandatory request parameters: *

      - *
    • Host url - can be set on the {@link ee.sk.smartid.SmartIdClient} level
    • + *
    • Host url - can be set on the {@link SmartIdClient} level
    • *
    • Relying party uuid - can either be set on the client or builder level
    • *
    • Relying party name - can either be set on the client or builder level
    • *
    • Either Document number or national identity
    • @@ -82,9 +90,9 @@ public CertificateRequestBuilder(SmartIdConnector connector, SessionStatusPoller * Sets the request's UUID of the relying party *

      * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} + * {@link SmartIdClient#setRelyingPartyUUID(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set the UUID every time when building a new request. * * @param relyingPartyUUID UUID of the relying party @@ -99,9 +107,9 @@ public CertificateRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { * Sets the request's name of the relying party *

      * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} + * {@link SmartIdClient#setRelyingPartyName(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set name every time when building a new request. * * @param relyingPartyName name of the relying party diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/v2/DigestCalculator.java similarity index 98% rename from src/main/java/ee/sk/smartid/DigestCalculator.java rename to src/main/java/ee/sk/smartid/v2/DigestCalculator.java index 6613323e..1d65ab48 100644 --- a/src/main/java/ee/sk/smartid/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/DigestCalculator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/HashType.java b/src/main/java/ee/sk/smartid/v2/HashType.java similarity index 98% rename from src/main/java/ee/sk/smartid/HashType.java rename to src/main/java/ee/sk/smartid/v2/HashType.java index 426c7f44..87754b68 100644 --- a/src/main/java/ee/sk/smartid/HashType.java +++ b/src/main/java/ee/sk/smartid/v2/HashType.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/v2/SignableData.java similarity index 97% rename from src/main/java/ee/sk/smartid/SignableData.java rename to src/main/java/ee/sk/smartid/v2/SignableData.java index b085ad0d..ab9555f3 100644 --- a/src/main/java/ee/sk/smartid/SignableData.java +++ b/src/main/java/ee/sk/smartid/v2/SignableData.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -40,7 +40,7 @@ * {@link #calculateHashInBase64()} methods * are used to calculate the hash for signing request. *

      - * {@link ee.sk.smartid.SignableHash} can be used + * {@link SignableHash} can be used * instead when the data to be signed is already * in hashed format. */ diff --git a/src/main/java/ee/sk/smartid/SignableHash.java b/src/main/java/ee/sk/smartid/v2/SignableHash.java similarity index 97% rename from src/main/java/ee/sk/smartid/SignableHash.java rename to src/main/java/ee/sk/smartid/v2/SignableHash.java index e3e92140..f4356b40 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/v2/SignableHash.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -38,7 +38,7 @@ * {@link #setHashType(HashType)} can be used * to set the hash type. *

      - * {@link ee.sk.smartid.SignableData} can be used + * {@link SignableData} can be used * instead when the data to be signed is not already * in hashed format. */ diff --git a/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java similarity index 94% rename from src/main/java/ee/sk/smartid/SignatureRequestBuilder.java rename to src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java index 043b2ccf..11e62d17 100644 --- a/src/main/java/ee/sk/smartid/SignatureRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -33,9 +33,17 @@ import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.*; +import ee.sk.smartid.v2.rest.dao.Capability; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.RequestProperties; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionSignature; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.SmartIdConnector; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +60,7 @@ *

      * Mandatory request parameters: *

        - *
      • Host url - can be set on the {@link ee.sk.smartid.SmartIdClient} level
      • + *
      • Host url - can be set on the {@link SmartIdClient} level
      • *
      • Relying party uuid - can either be set on the client or builder level
      • *
      • Relying party name - can either be set on the client or builder level
      • *
      • Document number
      • @@ -84,9 +92,9 @@ public SignatureRequestBuilder(SmartIdConnector connector, SessionStatusPoller s * Sets the request's UUID of the relying party *

        * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyUUID(String)} + * {@link SmartIdClient#setRelyingPartyUUID(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set the UUID every time when building a new request. * * @param relyingPartyUUID UUID of the relying party @@ -101,9 +109,9 @@ public SignatureRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { * Sets the request's name of the relying party *

        * If not for explicit need, it is recommended to use - * {@link ee.sk.smartid.SmartIdClient#setRelyingPartyName(String)} + * {@link SmartIdClient#setRelyingPartyName(String)} * instead. In that case when getting the builder from - * {@link ee.sk.smartid.SmartIdClient} it is not required + * {@link SmartIdClient} it is not required * to set name every time when building a new request. * * @param relyingPartyName name of the relying party @@ -159,7 +167,7 @@ public SignatureRequestBuilder withSemanticsIdentifier(SemanticsIdentifier seman *

        * This method could be used when the data * to be signed is not in hashed format. - * {@link ee.sk.smartid.SignableData#setHashType(HashType)} + * {@link SignableData#setHashType(HashType)} * can be used to select the wanted hash type * and the data is hashed for you. * diff --git a/src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java similarity index 99% rename from src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java rename to src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java index ede48dd9..75967db5 100644 --- a/src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/SmartIdCertificate.java b/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java similarity index 98% rename from src/main/java/ee/sk/smartid/SmartIdCertificate.java rename to src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java index 4887b134..a6f5654c 100644 --- a/src/main/java/ee/sk/smartid/SmartIdCertificate.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/v2/SmartIdClient.java similarity index 93% rename from src/main/java/ee/sk/smartid/SmartIdClient.java rename to src/main/java/ee/sk/smartid/v2/SmartIdClient.java index dce120b9..5b1d4446 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdClient.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -27,9 +27,9 @@ */ import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.SmartIdRestConnector; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; @@ -70,7 +70,7 @@ public class SmartIdClient { */ public CertificateRequestBuilder getCertificate() { SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - CertificateRequestBuilder builder = new CertificateRequestBuilder(getSmartIdConnector(), sessionStatusPoller); + var builder = new CertificateRequestBuilder(getSmartIdConnector(), sessionStatusPoller); builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); builder.withRelyingPartyName(this.getRelyingPartyName()); return builder; @@ -83,7 +83,7 @@ public CertificateRequestBuilder getCertificate() { */ public SignatureRequestBuilder createSignature() { SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - SignatureRequestBuilder builder = new SignatureRequestBuilder(getSmartIdConnector(), sessionStatusPoller); + var builder = new SignatureRequestBuilder(getSmartIdConnector(), sessionStatusPoller); builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); builder.withRelyingPartyName(this.getRelyingPartyName()); return builder; @@ -96,7 +96,7 @@ public SignatureRequestBuilder createSignature() { */ public AuthenticationRequestBuilder createAuthentication() { SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - AuthenticationRequestBuilder builder = new AuthenticationRequestBuilder(getSmartIdConnector(), sessionStatusPoller); + var builder = new AuthenticationRequestBuilder(getSmartIdConnector(), sessionStatusPoller); builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); builder.withRelyingPartyName(this.getRelyingPartyName()); return builder; @@ -213,7 +213,7 @@ public void setPollingSleepTimeout(TimeUnit unit, long timeout) { private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - SessionStatusPoller sessionStatusPoller = new SessionStatusPoller(connector); + var sessionStatusPoller = new SessionStatusPoller(connector); sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); return sessionStatusPoller; } @@ -258,7 +258,7 @@ public void setTrustSslContext(SSLContext trustSslContext) { public void setTrustStore(KeyStore trustStore) { try { SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + var trustManagerFactory = TrustManagerFactory.getInstance("X509"); trustManagerFactory.init(trustStore); trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); this.trustSslContext = trustSslContext; diff --git a/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java similarity index 89% rename from src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java rename to src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java index e1524d1d..b6e458b4 100644 --- a/src/main/java/ee/sk/smartid/SmartIdRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -30,12 +30,20 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.*; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionResult; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.SmartIdConnector; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/ee/sk/smartid/SmartIdSignature.java b/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java similarity index 99% rename from src/main/java/ee/sk/smartid/SmartIdSignature.java rename to src/main/java/ee/sk/smartid/v2/SmartIdSignature.java index 9fef3508..c903c9b8 100644 --- a/src/main/java/ee/sk/smartid/SmartIdSignature.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java similarity index 98% rename from src/main/java/ee/sk/smartid/VerificationCodeCalculator.java rename to src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java index f911d24a..798b619c 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java rename to src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java index efd280bc..29b4833c 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -31,7 +31,8 @@ import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SessionStatus; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java similarity index 82% rename from src/main/java/ee/sk/smartid/rest/SmartIdConnector.java rename to src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java index 75a94e8f..b294d46a 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -27,7 +27,14 @@ */ import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.*; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; import javax.net.ssl.SSLContext; import java.io.Serializable; diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java similarity index 95% rename from src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java rename to src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java index dcda6d34..b459695c 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -33,7 +33,16 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.*; +import ee.sk.smartid.rest.LoggingFilter; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SessionStatusRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; import jakarta.ws.rs.*; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; @@ -121,7 +130,7 @@ public CertificateChoiceResponse getCertificate(String documentNumber, Certifica @Override public CertificateChoiceResponse getCertificate(SemanticsIdentifier semanticsIdentifier, - CertificateRequest request) { + CertificateRequest request) { logger.debug("Getting certificate for identifier " + semanticsIdentifier.getIdentifier()); URI uri = UriBuilder .fromUri(endpointUrl) diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java similarity index 99% rename from src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java index bb1401e7..dd843a89 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java index ffd4dfe0..e344ad34 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/Capability.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/dao/Capability.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java index 38054fb0..a733200b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Capability.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceResponse.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java index bc33e261..2d68ef87 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java index c6bf6417..21db6785 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java similarity index 92% rename from src/main/java/ee/sk/smartid/rest/dao/Interaction.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java index 1fddf112..9fe41e0d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L @@ -26,10 +26,10 @@ * #L% */ -import static ee.sk.smartid.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; -import static ee.sk.smartid.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; -import static ee.sk.smartid.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.v2.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.v2.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.v2.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; +import static ee.sk.smartid.v2.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; import java.io.Serializable; diff --git a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java index 672800e0..48dfec1c 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java index ee798318..c2353480 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java index f1faadf5..2292eedc 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java index 0071fa49..ed889c0f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/SessionResult.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java index 1a6608dd..29e5351f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java index a16affbc..b96a024f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java index e0235ee2..c00b7418 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java index b3286e61..fdd93987 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java similarity index 99% rename from src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java index 81df1f66..69324578 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/rest/dao/SignatureSessionResponse.java rename to src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java index 168e885b..e1209a21 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java similarity index 98% rename from src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java rename to src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java index ce0aa48f..1283e92c 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid.v2.util; /*- * #%L @@ -52,7 +52,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.v2.AuthenticationIdentity; public class CertificateAttributeUtil { private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java similarity index 98% rename from src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java rename to src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java index f1f77da8..1faf0a19 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid.v2.util; /*- * #%L @@ -26,7 +26,7 @@ * #L% */ -import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.v2.AuthenticationIdentity; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java new file mode 100644 index 00000000..1d163025 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -0,0 +1,235 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.SmartIdRestConnector; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configuration; + +public class SmartIdClient { + + private String relyingPartyUUID; + private String relyingPartyName; + private String hostUrl; + private Configuration networkConnectionConfig; + private Client configuredClient; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + private long sessionStatusResponseSocketOpenTimeValue; + private SmartIdConnector connector; + private SSLContext trustSslContext; + + /** + * Sets the UUID of the relying party + *

        + * Can be set also on the builder level, + * but in that case it has to be set explicitly + * every time when building a new request. + * + * @param relyingPartyUUID UUID of the relying party + */ + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + /** + * Gets the UUID of the relying party + * + * @return UUID of the relying party + */ + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + /** + * Sets the name of the relying party + *

        + * Can be set also on the builder level, + * but in that case it has to be set + * every time when building a new request. + * + * @param relyingPartyName name of the relying party + */ + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + /** + * Gets the name of the relying party + * + * @return name of the relying party + */ + public String getRelyingPartyName() { + return relyingPartyName; + } + + /** + * Sets the base URL of the Smart-ID backend environment + *

        + * It defines the endpoint which the client communicates to. + * + * @param hostUrl base URL of the Smart-ID backend environment + */ + public void setHostUrl(String hostUrl) { + this.hostUrl = hostUrl; + } + + /** + * Sets the network connection configuration + *

        + * Useful for configuring network connection + * timeouts, proxy settings, request headers etc. + * + * @param networkConnectionConfig Jersey's network connection configuration instance + */ + public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { + this.networkConnectionConfig = networkConnectionConfig; + } + + public void setConfiguredClient(Client configuredClient) { + this.configuredClient = configuredClient; + } + + /** + * Sets the timeout for each session status poll + *

        + * Under the hood each operation (authentication, signing, choosing + * certificate) consists of 2 request steps: + *

        + * 1. Initiation request + *

        + * 2. Session status request + *

        + * Session status request is a long poll method, meaning + * the request method might not return until a timeout expires + * set by this parameter. + *

        + * Caller can tune the request parameters inside the bounds + * set by service operator. + *

        + * If not provided, a default is used. + * + * @param timeUnit time unit of the {@code timeValue} argument + * @param timeValue time value of each status poll's timeout. + */ + public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + sessionStatusResponseSocketOpenTimeUnit = timeUnit; + sessionStatusResponseSocketOpenTimeValue = timeValue; + } + + /** + * Sets the timeout/pause between each session status poll + * + * @param unit time unit of the {@code timeout} argument + * @param timeout timeout value in the given {@code unit} + */ + public void setPollingSleepTimeout(TimeUnit unit, long timeout) { + pollingSleepTimeUnit = unit; + pollingSleepTimeout = timeout; + } + + public SmartIdConnector getSmartIdConnector() { + if (null == connector) { + // Fallback to REST connector when not initialised + SmartIdRestConnector connector = configuredClient != null ? new SmartIdRestConnector(hostUrl, configuredClient) : new SmartIdRestConnector(hostUrl, networkConnectionConfig); + connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + + if (trustSslContext == null && configuredClient == null) { + throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); + } + + connector.setSslContext(this.trustSslContext); + setSmartIdConnector(connector); + } + return connector; + } + + public static SSLContext createSslContext(List sslCertificates) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + int i = 0; + for (String sslCertificate : sslCertificates) { + Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); + keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(keyStore); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext; + } + + public void setTrustSslContext(SSLContext trustSslContext) { + this.trustSslContext = trustSslContext; + } + + public void setTrustStore(KeyStore trustStore) { + try { + SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(trustStore); + trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); + this.trustSslContext = trustSslContext; + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); + } + } + + public void setTrustedCertificates(String... sslCertificates) { + try { + this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); + } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Failed to createSslContext", e); + } + } + + public void setSmartIdConnector(SmartIdConnector smartIdConnector) { + this.connector = smartIdConnector; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java new file mode 100644 index 00000000..77bea468 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java @@ -0,0 +1,51 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionStatusPoller { + + private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); + + private final SmartIdConnector connector; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + + public SessionStatusPoller(SmartIdConnector connector) { + this.connector = connector; + } + + public void setPollingSleepTime(TimeUnit unit, long timeout) { + logger.debug("Polling sleep time is " + timeout + " " + unit.toString()); + pollingSleepTimeUnit = unit; + pollingSleepTimeout = timeout; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java new file mode 100644 index 00000000..0439a832 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -0,0 +1,53 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +public interface SmartIdConnector extends Serializable { + + /** + * Set the session status response socket open time + * + * @param sessionStatusResponseSocketOpenTimeUnit The time unit of the open time + * @param sessionStatusResponseSocketOpenTimeValue The value of the open time + */ + void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); + + /** + * Set the SSL context to use for secure communication + * + * @param sslContext The SSL context + */ + void setSslContext(SSLContext sslContext); +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java new file mode 100644 index 00000000..9c5159b7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -0,0 +1,203 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + +import java.io.Serial; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.rest.LoggingFilter; +import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; + +public class SmartIdRestConnector implements SmartIdConnector { + + @Serial + private static final long serialVersionUID = 44L; + + private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); + + private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + + private final String endpointUrl; + private transient Configuration clientConfig; + private transient Client configuredClient; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + private long sessionStatusResponseSocketOpenTimeValue; + private transient SSLContext sslContext; + + public SmartIdRestConnector(String endpointUrl) { + this.endpointUrl = endpointUrl; + } + + public SmartIdRestConnector(String endpointUrl, Configuration clientConfig) { + this(endpointUrl); + this.clientConfig = clientConfig; + } + + public SmartIdRestConnector(String endpointUrl, Client configuredClient) { + this(endpointUrl); + this.configuredClient = configuredClient; + } + + private SessionStatus getSessionStatus(String sessionId, String path) throws SessionNotFoundException { + SessionStatusRequest request = createSessionStatusRequest(sessionId); + UriBuilder uriBuilder = UriBuilder.fromUri(endpointUrl).path(path); + addResponseSocketOpenTimeUrlParameter(request, uriBuilder); + URI uri = uriBuilder.build(request.getSessionId()); + try { + return prepareClient(uri).get(SessionStatus.class); + } catch (NotFoundException e) { + logger.warn("Session " + request + " not found: " + e.getMessage()); + throw new SessionNotFoundException(); + } + } + + private T postRequest(URI uri, V request, Class responseType) { + try { + Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); + return prepareClient(uri).post(requestEntity, responseType); + } catch (NotAuthorizedException e) { + logger.warn("Request is unauthorized for URI " + uri, e); + throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); + } catch (BadRequestException e) { + logger.warn("Request is invalid for URI " + uri, e); + throw new SmartIdClientException("Server refused the request", e); + } catch (ClientErrorException e) { + if (e.getResponse().getStatus() == 471) { + logger.warn("No suitable account of requested type found, but user has some other accounts.", e); + throw new NoSuitableAccountOfRequestedTypeFoundException(); + } + if (e.getResponse().getStatus() == 472) { + logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", e); + throw new PersonShouldViewSmartIdPortalException(); + } + if (e.getResponse().getStatus() == 480) { + logger.warn("Client-side API is too old and not supported anymore"); + throw new SmartIdClientException("Client-side API is too old and not supported anymore"); + } + throw e; + } catch (ServerErrorException e) { + if (e.getResponse().getStatus() == 580) { + logger.warn("Server is under maintenance, retry later", e); + throw new ServerMaintenanceException(); + } + throw e; + } + } + + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + var request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; + } + + private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { + if (request.isResponseSocketOpenTimeSet()) { + long queryTimeoutInMilliseconds = sessionStatusResponseSocketOpenTimeUnit.toMillis(sessionStatusResponseSocketOpenTimeValue); + uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); + } + } + + protected Invocation.Builder prepareClient(URI uri) { + Client client; + if (this.configuredClient == null) { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (clientConfig != null) { + clientBuilder.withConfig(clientConfig); + } + if (sslContext != null) { + clientBuilder.sslContext(sslContext); + } + client = clientBuilder.build(); + } else { + client = this.configuredClient; + } + + return client + .register(new LoggingFilter()) + .target(uri) + .request() + .accept(APPLICATION_JSON_TYPE) + .header("User-Agent", buildUserAgentString()); + } + + protected String buildUserAgentString() { + return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; + } + + protected String getClientVersion() { + String clientVersion = getClass().getPackage().getImplementationVersion(); + return clientVersion == null ? "-" : clientVersion; + } + + protected String getJdkMajorVersion() { + try { + return System.getProperty("java.version").split("_")[0]; + } catch (Exception e) { + return "-"; + } + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; + } + + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java new file mode 100644 index 00000000..ee59df02 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java @@ -0,0 +1,132 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2022 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.v3.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.v3.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.v3.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; +import static ee.sk.smartid.v3.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Interaction implements Serializable { + + private InteractionFlow type; + + private String displayText60; + private String displayText200; + + private Interaction(InteractionFlow type) { + this.type = type; + } + + public static Interaction displayTextAndPIN(String displayText60) { + Interaction interaction = new Interaction(DISPLAY_TEXT_AND_PIN); + interaction.displayText60 = displayText60; + return interaction; + } + + public static Interaction verificationCodeChoice(String displayText60) { + Interaction interaction = new Interaction(VERIFICATION_CODE_CHOICE); + interaction.displayText60 = displayText60; + return interaction; + } + + public static Interaction confirmationMessage(String displayText200) { + Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE); + interaction.displayText200 = displayText200; + return interaction; + } + + public static Interaction confirmationMessageAndVerificationCodeChoice(String displayText200) { + Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); + interaction.displayText200 = displayText200; + return interaction; + } + + public InteractionFlow getType() { + return type; + } + + public void setType(InteractionFlow type) { + this.type = type; + } + + public String getDisplayText60() { + return displayText60; + } + + public void setDisplayText60(String displayText60) { + this.displayText60 = displayText60; + } + + public String getDisplayText200() { + return displayText200; + } + + public void setDisplayText200(String displayText200) { + this.displayText200 = displayText200; + } + + public void validate() { + validateDisplayText60(); + validateDisplayText200(); + } + + private void validateDisplayText60() { + if (getType() == VERIFICATION_CODE_CHOICE || getType() == DISPLAY_TEXT_AND_PIN) { + if (getDisplayText60() == null) { + throw new SmartIdClientException("displayText60 cannot be null for AllowedInteractionOrder of type " + getType()); + } + if (getDisplayText60().length() > 60) { + throw new SmartIdClientException("displayText60 must not be longer than 60 characters"); + } + if (getDisplayText200() != null) { + throw new SmartIdClientException("displayText200 must be null for AllowedInteractionOrder of type " + getType()); + } + } + } + + private void validateDisplayText200() { + if (getType() == CONFIRMATION_MESSAGE || getType() == CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { + if (getDisplayText200() == null) { + throw new SmartIdClientException("displayText200 cannot be null for AllowedInteractionOrder of type " + getType()); + } + if (getDisplayText200().length() > 200) { + throw new SmartIdClientException("displayText200 must not be longer than 200 characters"); + } + if (getDisplayText60() != null) { + throw new SmartIdClientException("displayText60 must be null for AllowedInteractionOrder of type " + getType()); + } + } + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java new file mode 100644 index 00000000..7e07ea57 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java @@ -0,0 +1,53 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2022 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum InteractionFlow { + + DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), + CONFIRMATION_MESSAGE("confirmationMessage"), + VERIFICATION_CODE_CHOICE("verificationCodeChoice"), + CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); + + private final String code; + + InteractionFlow(String code) { + this.code = code; + } + + @JsonValue + public String getCode() { + return code; + } + + public boolean is(String typeCodeString) { + return this.getCode().equals(typeCodeString); + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java new file mode 100644 index 00000000..794d0102 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java @@ -0,0 +1,52 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +public class RequestProperties implements Serializable { + + @JsonInclude(JsonInclude.Include.NON_NULL) + Boolean shareMdClientIpAddress; + + public Boolean getShareMdClientIpAddress() { + return shareMdClientIpAddress; + } + + public void setShareMdClientIpAddress(Boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + } + + @JsonIgnore + public boolean hasProperties() { + return shareMdClientIpAddress != null; + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java new file mode 100644 index 00000000..56ac9b0c --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java @@ -0,0 +1,70 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class SemanticsIdentifier implements Serializable { + + private final String identifier; + + public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { + this.identifier = "" + identityType + countryCode + "-" + identityNumber; + } + + public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { + this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; + } + + public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { + this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; + } + + public SemanticsIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + + public enum IdentityType { + PAS, IDC, PNO + } + + public enum CountryCode { + EE, LT, LV + } + + @Override + public String toString() { + return "SemanticsIdentifier{" + + "identifier='" + identifier + '\'' + + '}'; + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java new file mode 100644 index 00000000..ba260090 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResult implements Serializable { + + private String endResult; + private String documentNumber; + + public String getDocumentNumber() { + return documentNumber; + } + + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getEndResult() { + return endResult; + } + + public void setEndResult(String endResult) { + this.endResult = endResult; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java new file mode 100644 index 00000000..1f66abdd --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java @@ -0,0 +1,97 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import ee.sk.smartid.v2.rest.dao.SessionCertificate; +import ee.sk.smartid.v2.rest.dao.SessionSignature; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionStatus implements Serializable { + + private String state; + private SessionResult result; + + private String[] ignoredProperties = {}; + + private String interactionFlowUsed; + private String deviceIpAddress; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public SessionResult getResult() { + return result; + } + + public void setResult(SessionResult result) { + this.result = result; + } + + public String[] getIgnoredProperties() { + return Arrays.copyOf(ignoredProperties, ignoredProperties.length); + } + + public void setIgnoredProperties(String[] ignoredProperties) { + this.ignoredProperties = Arrays.copyOf(ignoredProperties, ignoredProperties.length); + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + /** + * IP-address of the device running the App. + *

        + * Present only if withShareMdClientIpAddress() was specified with the request + * Also, the RelyingParty must be subscribed for the service. + * Also, the data must be available (e.g. not present in case state is TIMEOUT). + * @see Mobile Device IP sharing + * + * @return IP address of the device running Smart-ID app (or null if not returned) + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java new file mode 100644 index 00000000..e394c135 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java @@ -0,0 +1,73 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +public class SessionStatusRequest implements Serializable { + + private final String sessionId; + private TimeUnit responseSocketOpenTimeUnit; + private long responseSocketOpenTimeValue; + + public SessionStatusRequest(String sessionId) { + this.sessionId = sessionId; + } + + public String getSessionId() { + return sessionId; + } + + /** + * Request long poll timeout value. If not provided, a default is used. + *

        + * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires + * set by this parameter. + *

        + * Caller can tune the request parameters inside the bounds set by service operator. + * + * @param timeUnit time unit of how much time a network request socket should be kept open. + * @param timeValue time value of how much time a network request socket should be kept open. + */ + public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + responseSocketOpenTimeUnit = timeUnit; + responseSocketOpenTimeValue = timeValue; + } + + public boolean isResponseSocketOpenTimeSet() { + return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; + } + + public TimeUnit getResponseSocketOpenTimeUnit() { + return responseSocketOpenTimeUnit; + } + + public long getResponseSocketOpenTimeValue() { + return responseSocketOpenTimeValue; + } +} diff --git a/src/test/java/ee/sk/smartid/DummyData.java b/src/test/java/ee/sk/smartid/DummyData.java deleted file mode 100644 index 2cd0966d..00000000 --- a/src/test/java/ee/sk/smartid/DummyData.java +++ /dev/null @@ -1,65 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionStatus; - -public class DummyData { - - public static final String CERTIFICATE = "MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTYxMjA5MTYyNDU2WhcNMTkxMjA5MTYyNDU2WjCBvzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMS0wKwYDVQQDDCRFTEZSSUlEQSxNQU5JVkFMREUsUE5PRUUtMzExMTExMTExMTExETAPBgNVBAQMCEVMRlJJSURBMRIwEAYDVQQqDAlNQU5JVkFMREUxGjAYBgNVBAUTEVBOT0VFLTMxMTExMTExMTExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgcfk+eY6dvVyDDPpJPkoKpQ08pQx5Jpfjgq+G31lRSsx03y4WYWQhILu5R4isI6DGzQ1MK2dEsW9Dl+S39y7mDDqGlviVpxCtgz14H7NG84ew8vd+sBeaYCvEhKS4+FxRWCmg5VCozr3s2Evi/ao3Wj51ThtecVmAY7PoE27Zckr0GJ/0I+JqEQx19POBr/lNkZN1AxBy8O9gvDzdpCa2Vn9qahY9eZnDGScrP2KsR34UlXa5PjEMVPtSB4btPi9VOQuRoZImGchfUyf1A2uyIPhV5aC+Zgl60B65WxZ+/nEsVN4NoSgBUv+HlwuRxJPelQKeA9tPwKroqO9PGc5/ee2C1HLH7loD+SwahSPMOY2e8CQd6pLmRF1a/H+ZPWZBW+U7Ekm3YeNNJToUkuonAQB/JbwBvHkZXwsH4/kMHyMPiws5G3nr/jyqF2595KKghIgjGHR1WhGljQzdgO5LT4uuOhesGDRYeMUanvClWSb/mt0SdS8njziY7WoYPYFFFgjRvIIK5FgOd8d2W88I5pj2/SjcXb6GMqEqI3HkCBGPDSo57nSJZzJD8KjJs/4jvzZnGwCFZ8+jeyh562B01mkFfwFaoFOYfqRG3g5sGdZUdY9Nk3FZ8dgEwylUMSxmaL0R2/mzNVasFWp482eHwlK2rae3v+QtCHGfOKn+vsCAwEAAaOCAdIwggHOMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMFYGA1UdIARPME0wQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABATAdBgNVHQ4EFgQUNxW1gjoB4+Qh46Rj3SuULubhtUMwgZkGCCsGAQUFBwEDBIGMMIGJMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCH+SY8KKgw5UDlVL99ToRWPpcloyfOM64UTnNgEDXDDI5r1CNNA0OlggzoEZfakNQJamHjIT287LV7nXFsB4Q9VzyI3H1J5mzVIZrMUiE68wf25BDuA3Zwpri+f8Me78f3nowO2cJ2AiMJ83vQFKKy1LFOixWguuxioKlda2Jq7B57ty5cN+jZwLO7Vrv4Tryg9QeOaxnFvHvuZaxMnE55of7cLpfyAH/5DKvlXx4cdmh7kNO4F/o2LT7om4Cf+Sq6tFS3cUn4zcQbFKT5lw+7vfewzG6X0qYnHbe7Ts/zhh7IJpHnPF1p23ND0+jHgbcDVTFjV4pN1PhVthYHOMeDW461okw2OA/jfuQetUlDwqT5yCdjrOTMDkjZCjTMhcVPzw+7hSUUnewKiR0smuyZbKpE/ZGZWUA6K0sieGCpHGKJo99zD3zmEWmOmq++D0TmVvEiXVJs8fuNWl+VmXSStkMeNR4noHAL1PFUebXVS0lPpQZzBKgqhMGAgbwvYajZnOlvXVll6QashxFZmOVNy88O67s+a2p1SmQTtqNrlodszqkKsc28nDbbvBUd4PUD5tmVgPe29Zwnm1TxFuhl0gqvVc+qZme8zq6yd3nCKNrY6qron4Xcc1rxCWS7NcyO5JiF+qXgAuDOkSFJaaEnQh83ZJsNneXD/nyBH8kSiQ=="; - - public static SessionResult createSessionEndResult() { - SessionResult result = createSessionResult("OK"); - result.setDocumentNumber("PNOEE-31111111111"); - return result; - } - - public static SessionStatus createUserRefusedSessionStatus(String sessionResult) { - SessionStatus status = createCompleteSessionStatus(); - status.setResult(createSessionResult(sessionResult)); - return status; - } - - public static SessionStatus createUserSelectedWrongVerificationCode() { - SessionStatus status = createCompleteSessionStatus(); - status.setResult(createSessionResult("WRONG_VC")); - return status; - } - - public static SessionResult createSessionResult(String endResult) { - SessionResult result = new SessionResult(); - result.setEndResult(endResult); - return result; - } - - public static SessionStatus createCompleteSessionStatus() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - return status; - } -} diff --git a/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java similarity index 96% rename from src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java rename to src/test/java/ee/sk/smartid/integration/ReadmeTest.java index 06a53353..9cabdc1a 100644 --- a/src/test/java/ee/sk/test/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java @@ -1,4 +1,4 @@ -package ee.sk.test.smartid.integration; +package ee.sk.smartid.integration; /*- * #%L @@ -26,9 +26,9 @@ * #L% */ -import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.assertAuthenticationResponseCreated; -import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.createAuthenticationSessionRequest; -import static ee.sk.smartid.rest.SmartIdRestIntegrationTest.pollSessionStatus; +import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.assertAuthenticationResponseCreated; +import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.createAuthenticationSessionRequest; +import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.pollSessionStatus; import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyOrNullString; @@ -57,29 +57,29 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.FileUtil; -import ee.sk.SmartIdDemoIntegrationTest; -import ee.sk.smartid.AuthenticationHash; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationResponseValidator; -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.HashType; -import ee.sk.smartid.SignableHash; -import ee.sk.smartid.SmartIdAuthenticationResponse; -import ee.sk.smartid.SmartIdCertificate; -import ee.sk.smartid.SmartIdClient; -import ee.sk.smartid.SmartIdSignature; +import ee.sk.smartid.v2.AuthenticationHash; +import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.v2.AuthenticationResponseValidator; +import ee.sk.smartid.v2.CertificateParser; +import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.FileUtil; +import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.v2.SignableHash; +import ee.sk.smartid.v2.SmartIdAuthenticationResponse; +import ee.sk.smartid.v2.SmartIdCertificate; +import ee.sk.smartid.v2.SmartIdClient; +import ee.sk.smartid.v2.SmartIdSignature; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.InteractionFlow; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.InteractionFlow; +import ee.sk.smartid.v2.rest.dao.SessionStatus; /** * These tests contain snippets used in Readme.md diff --git a/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java similarity index 98% rename from src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java rename to src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java index c725e64c..dd4262ca 100644 --- a/src/test/java/ee/sk/test/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.test.smartid.integration; +package ee.sk.smartid.integration; /*- * #%L @@ -42,18 +42,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.FileUtil; -import ee.sk.SmartIdDemoIntegrationTest; -import ee.sk.smartid.AuthenticationHash; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationResponseValidator; -import ee.sk.smartid.SignableData; -import ee.sk.smartid.SmartIdAuthenticationResponse; -import ee.sk.smartid.SmartIdCertificate; -import ee.sk.smartid.SmartIdClient; -import ee.sk.smartid.SmartIdSignature; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.AuthenticationHash; +import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.v2.AuthenticationResponseValidator; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.FileUtil; +import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.v2.SignableData; +import ee.sk.smartid.v2.SmartIdAuthenticationResponse; +import ee.sk.smartid.v2.SmartIdCertificate; +import ee.sk.smartid.v2.SmartIdClient; +import ee.sk.smartid.v2.SmartIdSignature; +import ee.sk.smartid.v2.rest.dao.Interaction; @SmartIdDemoIntegrationTest public class SmartIdIntegrationTest { diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdConnectorSpy.java b/src/test/java/ee/sk/smartid/rest/SmartIdConnectorSpy.java deleted file mode 100644 index 8f64e115..00000000 --- a/src/test/java/ee/sk/smartid/rest/SmartIdConnectorSpy.java +++ /dev/null @@ -1,107 +0,0 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.*; - -import javax.net.ssl.SSLContext; -import java.util.concurrent.TimeUnit; - -public class SmartIdConnectorSpy implements SmartIdConnector { - - public SessionStatus sessionStatusToRespond; - public CertificateChoiceResponse certificateChoiceToRespond; - public SignatureSessionResponse signatureSessionResponseToRespond; - public AuthenticationSessionResponse authenticationSessionResponseToRespond; - - public String sessionIdUsed; - public SemanticsIdentifier semanticsIdentifierUsed; - public String documentNumberUsed; - public CertificateRequest certificateRequestUsed; - public SignatureSessionRequest signatureSessionRequestUsed; - public AuthenticationSessionRequest authenticationSessionRequestUsed; - - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - sessionIdUsed = sessionId; - return sessionStatusToRespond; - } - - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - documentNumberUsed = documentNumber; - certificateRequestUsed = request; - return certificateChoiceToRespond; - } - - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, CertificateRequest request) { - semanticsIdentifierUsed = identifier; - certificateRequestUsed = request; - return certificateChoiceToRespond; - } - - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - documentNumberUsed = documentNumber; - signatureSessionRequestUsed = request; - return signatureSessionResponseToRespond; - } - - @Override - public SignatureSessionResponse sign(SemanticsIdentifier identifier, SignatureSessionRequest request) { - semanticsIdentifierUsed = identifier; - signatureSessionRequestUsed = request; - return signatureSessionResponseToRespond; - } - - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - documentNumberUsed = documentNumber; - authenticationSessionRequestUsed = request; - return authenticationSessionResponseToRespond; - } - - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier identifier, AuthenticationSessionRequest request) { - semanticsIdentifierUsed = identifier; - authenticationSessionRequestUsed = request; - return authenticationSessionResponseToRespond; - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - - } - - @Override - public void setSslContext(SSLContext sslContext) { - - } -} diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java rename to src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java index a8e5012c..66735c9f 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.AuthenticationIdentity; + public class AuthenticationIdentityTest { @SuppressWarnings("deprecation") diff --git a/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java similarity index 92% rename from src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java index 479ef871..46c5d72d 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -26,9 +26,9 @@ * #L% */ -import static ee.sk.smartid.DummyData.createSessionEndResult; -import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; -import static ee.sk.smartid.DummyData.createUserSelectedWrongVerificationCode; +import static ee.sk.smartid.v2.DummyData.createSessionEndResult; +import static ee.sk.smartid.v2.DummyData.createUserRefusedSessionStatus; +import static ee.sk.smartid.v2.DummyData.createUserSelectedWrongVerificationCode; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,6 +41,7 @@ import java.util.Collections; import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,15 +54,15 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.Capability; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.Capability; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionCertificate; +import ee.sk.smartid.v2.rest.dao.SessionSignature; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SessionStatusPoller; public class AuthenticationRequestBuilderTest { @@ -188,8 +189,8 @@ public void authenticate_withShareMdClientIpAddressTrue() throws Exception { .withShareMdClientIpAddress(true) .authenticate(); - assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertTrue(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); + Assertions.assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertTrue(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); assertCorrectSessionRequestMade(); @@ -212,9 +213,9 @@ public void authenticate_withShareMdClientIpAddressFalse() throws Exception { assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertFalse(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); + Assertions.assertFalse(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); assertCorrectSessionRequestMade(); assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); @@ -554,20 +555,20 @@ public void authenticate_certificateMissingInResponse_shouldThrowException() { private void assertCorrectAuthenticationRequestMadeWithDocumentNumber(String expectedHashToSignInBase64, String expectedCertificateLevel) { assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); + Assertions.assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); + Assertions.assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); + Assertions.assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); + Assertions.assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); + Assertions.assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); } private void assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(String expectedHashToSignInBase64, String expectedCertificateLevel) { - assertEquals("IDCCZ-1234567890", connector.semanticsIdentifierUsed.getIdentifier()); - assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); + Assertions.assertEquals("IDCCZ-1234567890", connector.semanticsIdentifierUsed.getIdentifier()); + Assertions.assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); + Assertions.assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); + Assertions.assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); + Assertions.assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); + Assertions.assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); } private void assertCorrectSessionRequestMade() { diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java rename to src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java index b7feab18..6a7b31af 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -49,7 +49,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.CertificateUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; diff --git a/src/test/java/ee/sk/smartid/CertificateLevelTest.java b/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/CertificateLevelTest.java rename to src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java index 58b0b1d9..b8c6f4ec 100644 --- a/src/test/java/ee/sk/smartid/CertificateLevelTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.CertificateLevel; + public class CertificateLevelTest { @Test diff --git a/src/test/java/ee/sk/smartid/CertificateParserTest.java b/src/test/java/ee/sk/smartid/v2/CertificateParserTest.java similarity index 95% rename from src/test/java/ee/sk/smartid/CertificateParserTest.java rename to src/test/java/ee/sk/smartid/v2/CertificateParserTest.java index 9d4b70d5..990ae88e 100644 --- a/src/test/java/ee/sk/smartid/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateParserTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v2.CertificateParser; public class CertificateParserTest { diff --git a/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java similarity index 86% rename from src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java index 24a0abc9..553ad317 100644 --- a/src/test/java/ee/sk/smartid/CertificateRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -26,8 +26,6 @@ * #L% */ -import static ee.sk.smartid.DummyData.createSessionEndResult; -import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,19 +43,21 @@ import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.Capability; -import ee.sk.smartid.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; +import ee.sk.smartid.v2.rest.dao.Capability; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionCertificate; +import ee.sk.smartid.v2.rest.dao.SessionStatus; public class CertificateRequestBuilderTest { @@ -139,8 +139,8 @@ public void getCertificate_withShareMdClientIpAddressTrue() { .fetch(); assertCertificateResponseValid(certificate); - assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertTrue(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); + Assertions.assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertTrue(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); assertThat(certificate.getDeviceIpAddress(), is("5.5.5.5")); assertCorrectSessionRequestMade(); @@ -158,8 +158,8 @@ public void getCertificate_withShareMdClientIpAddressFalse() { .fetch(); assertCertificateResponseValid(certificate); - assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertFalse(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); + Assertions.assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertFalse(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); assertCorrectSessionRequestMade(); assertValidCertificateChoiceRequestMade("ADVANCED"); @@ -224,7 +224,7 @@ public void getCertificate_withCapabilities() { @Test public void getCertificate_whenUserRefuses_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + connector.sessionStatusToRespond = DummyData.createUserRefusedSessionStatus("USER_REFUSED"); makeCertificateRequest(); }); } @@ -232,7 +232,7 @@ public void getCertificate_whenUserRefuses_shouldThrowException() { @Test public void getCertificate_withDocumentNumber_whenUserRefuses_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); + connector.sessionStatusToRespond = DummyData.createUserRefusedSessionStatus("USER_REFUSED"); builder .withRelyingPartyUUID("relying-party-uuid") .withRelyingPartyName("relying-party-name") @@ -289,25 +289,25 @@ private void assertCorrectSessionRequestMade() { } private void assertValidCertificateChoiceRequestMade(String certificateLevel) { - assertThat(connector.semanticsIdentifierUsed.getIdentifier(), is("PNOEE-31111111111")); + MatcherAssert.assertThat(connector.semanticsIdentifierUsed.getIdentifier(), is("PNOEE-31111111111")); - assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); + Assertions.assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); + Assertions.assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); + Assertions.assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); } private void assertValidCertificateRequestMadeWithDocumentNumber(String certificateLevel) { assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); + Assertions.assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); + Assertions.assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); + Assertions.assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); } private SessionStatus createCertificateSessionStatusCompleteResponse() { SessionStatus status = new SessionStatus(); status.setState("COMPLETE"); status.setCert(createSessionCertificate()); - status.setResult(createSessionEndResult()); + status.setResult(DummyData.createSessionEndResult()); status.setDeviceIpAddress("5.5.5.5"); return status; } diff --git a/src/test/java/ee/sk/CertificateUtil.java b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java similarity index 96% rename from src/test/java/ee/sk/CertificateUtil.java rename to src/test/java/ee/sk/smartid/v2/CertificateUtil.java index 59f0275e..03fb9c56 100644 --- a/src/test/java/ee/sk/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java @@ -1,4 +1,4 @@ -package ee.sk; +package ee.sk.smartid.v2; /*- * #%L @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,7 +31,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.v2.CertificateParser; public final class CertificateUtil { diff --git a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java similarity index 73% rename from src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java rename to src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java index 67770136..becda236 100644 --- a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,26 +26,26 @@ * #L% */ +import java.util.Map; + import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; import jakarta.ws.rs.core.MultivaluedMap; -import java.util.Map; - public class ClientRequestHeaderFilter implements ClientRequestFilter { - private final Map headersToAdd; + private final Map headersToAdd; - public ClientRequestHeaderFilter(Map headersToAdd) { - this.headersToAdd = headersToAdd; - } + public ClientRequestHeaderFilter(Map headersToAdd) { + this.headersToAdd = headersToAdd; + } - @Override - public void filter(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getHeaders(); - for (Map.Entry entry : headersToAdd.entrySet()) { - headers.putSingle(entry.getKey(), entry.getValue()); + @Override + public void filter(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + for (Map.Entry entry : headersToAdd.entrySet()) { + headers.putSingle(entry.getKey(), entry.getValue()); + } } - } } diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java similarity index 96% rename from src/test/java/ee/sk/smartid/DigestCalculatorTest.java rename to src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java index eed4a309..b83dcf38 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.v2.DigestCalculator; +import ee.sk.smartid.v2.HashType; public class DigestCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/v2/DummyData.java b/src/test/java/ee/sk/smartid/v2/DummyData.java new file mode 100644 index 00000000..5c735181 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v2/DummyData.java @@ -0,0 +1,65 @@ +package ee.sk.smartid.v2; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.v2.rest.dao.SessionResult; +import ee.sk.smartid.v2.rest.dao.SessionStatus; + +public class DummyData { + + public static final String CERTIFICATE = "MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTYxMjA5MTYyNDU2WhcNMTkxMjA5MTYyNDU2WjCBvzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMS0wKwYDVQQDDCRFTEZSSUlEQSxNQU5JVkFMREUsUE5PRUUtMzExMTExMTExMTExETAPBgNVBAQMCEVMRlJJSURBMRIwEAYDVQQqDAlNQU5JVkFMREUxGjAYBgNVBAUTEVBOT0VFLTMxMTExMTExMTExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgcfk+eY6dvVyDDPpJPkoKpQ08pQx5Jpfjgq+G31lRSsx03y4WYWQhILu5R4isI6DGzQ1MK2dEsW9Dl+S39y7mDDqGlviVpxCtgz14H7NG84ew8vd+sBeaYCvEhKS4+FxRWCmg5VCozr3s2Evi/ao3Wj51ThtecVmAY7PoE27Zckr0GJ/0I+JqEQx19POBr/lNkZN1AxBy8O9gvDzdpCa2Vn9qahY9eZnDGScrP2KsR34UlXa5PjEMVPtSB4btPi9VOQuRoZImGchfUyf1A2uyIPhV5aC+Zgl60B65WxZ+/nEsVN4NoSgBUv+HlwuRxJPelQKeA9tPwKroqO9PGc5/ee2C1HLH7loD+SwahSPMOY2e8CQd6pLmRF1a/H+ZPWZBW+U7Ekm3YeNNJToUkuonAQB/JbwBvHkZXwsH4/kMHyMPiws5G3nr/jyqF2595KKghIgjGHR1WhGljQzdgO5LT4uuOhesGDRYeMUanvClWSb/mt0SdS8njziY7WoYPYFFFgjRvIIK5FgOd8d2W88I5pj2/SjcXb6GMqEqI3HkCBGPDSo57nSJZzJD8KjJs/4jvzZnGwCFZ8+jeyh562B01mkFfwFaoFOYfqRG3g5sGdZUdY9Nk3FZ8dgEwylUMSxmaL0R2/mzNVasFWp482eHwlK2rae3v+QtCHGfOKn+vsCAwEAAaOCAdIwggHOMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMFYGA1UdIARPME0wQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABATAdBgNVHQ4EFgQUNxW1gjoB4+Qh46Rj3SuULubhtUMwgZkGCCsGAQUFBwEDBIGMMIGJMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCH+SY8KKgw5UDlVL99ToRWPpcloyfOM64UTnNgEDXDDI5r1CNNA0OlggzoEZfakNQJamHjIT287LV7nXFsB4Q9VzyI3H1J5mzVIZrMUiE68wf25BDuA3Zwpri+f8Me78f3nowO2cJ2AiMJ83vQFKKy1LFOixWguuxioKlda2Jq7B57ty5cN+jZwLO7Vrv4Tryg9QeOaxnFvHvuZaxMnE55of7cLpfyAH/5DKvlXx4cdmh7kNO4F/o2LT7om4Cf+Sq6tFS3cUn4zcQbFKT5lw+7vfewzG6X0qYnHbe7Ts/zhh7IJpHnPF1p23ND0+jHgbcDVTFjV4pN1PhVthYHOMeDW461okw2OA/jfuQetUlDwqT5yCdjrOTMDkjZCjTMhcVPzw+7hSUUnewKiR0smuyZbKpE/ZGZWUA6K0sieGCpHGKJo99zD3zmEWmOmq++D0TmVvEiXVJs8fuNWl+VmXSStkMeNR4noHAL1PFUebXVS0lPpQZzBKgqhMGAgbwvYajZnOlvXVll6QashxFZmOVNy88O67s+a2p1SmQTtqNrlodszqkKsc28nDbbvBUd4PUD5tmVgPe29Zwnm1TxFuhl0gqvVc+qZme8zq6yd3nCKNrY6qron4Xcc1rxCWS7NcyO5JiF+qXgAuDOkSFJaaEnQh83ZJsNneXD/nyBH8kSiQ=="; + + public static SessionResult createSessionEndResult() { + SessionResult result = createSessionResult("OK"); + result.setDocumentNumber("PNOEE-31111111111"); + return result; + } + + public static SessionStatus createUserRefusedSessionStatus(String sessionResult) { + SessionStatus status = createCompleteSessionStatus(); + status.setResult(createSessionResult(sessionResult)); + return status; + } + + public static SessionStatus createUserSelectedWrongVerificationCode() { + SessionStatus status = createCompleteSessionStatus(); + status.setResult(createSessionResult("WRONG_VC")); + return status; + } + + public static SessionResult createSessionResult(String endResult) { + SessionResult result = new SessionResult(); + result.setEndResult(endResult); + return result; + } + + public static SessionStatus createCompleteSessionStatus() { + SessionStatus status = new SessionStatus(); + status.setState("COMPLETE"); + return status; + } +} diff --git a/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java rename to src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java index 03ea6fdc..ef8f955e 100644 --- a/src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -42,9 +42,8 @@ import org.junit.jupiter.api.Test; -import ee.sk.FileUtil; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.test.smartid.integration.SmartIdIntegrationTest; +import ee.sk.smartid.integration.SmartIdIntegrationTest; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; diff --git a/src/test/java/ee/sk/FileUtil.java b/src/test/java/ee/sk/smartid/v2/FileUtil.java similarity index 98% rename from src/test/java/ee/sk/FileUtil.java rename to src/test/java/ee/sk/smartid/v2/FileUtil.java index 0fd31e3c..fd307779 100644 --- a/src/test/java/ee/sk/FileUtil.java +++ b/src/test/java/ee/sk/smartid/v2/FileUtil.java @@ -1,4 +1,4 @@ -package ee.sk; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/v2/SignableDataTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/SignableDataTest.java rename to src/test/java/ee/sk/smartid/v2/SignableDataTest.java index c60fea3c..813fb830 100644 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignableDataTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -32,6 +32,9 @@ import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.v2.SignableData; + public class SignableDataTest { private static final byte[] DATA_TO_SIGN = "Hello World!".getBytes(); diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java similarity index 93% rename from src/test/java/ee/sk/smartid/SignableHashTest.java rename to src/test/java/ee/sk/smartid/v2/SignableHashTest.java index bddca5b0..364c8590 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -30,6 +30,10 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.DigestCalculator; +import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.v2.SignableHash; + public class SignableHashTest { @Test diff --git a/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java similarity index 93% rename from src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java index ad93d37c..4e756b20 100644 --- a/src/test/java/ee/sk/smartid/SignatureRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -26,9 +26,9 @@ * #L% */ -import static ee.sk.smartid.DummyData.createSessionEndResult; -import static ee.sk.smartid.DummyData.createUserRefusedSessionStatus; -import static ee.sk.smartid.DummyData.createUserSelectedWrongVerificationCode; +import static ee.sk.smartid.v2.DummyData.createSessionEndResult; +import static ee.sk.smartid.v2.DummyData.createUserRefusedSessionStatus; +import static ee.sk.smartid.v2.DummyData.createUserSelectedWrongVerificationCode; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -40,6 +40,7 @@ import java.util.Collections; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,13 +53,13 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnectorSpy; -import ee.sk.smartid.rest.dao.Capability; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SessionSignature; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.dao.Capability; public class SignatureRequestBuilderTest { @@ -155,9 +156,9 @@ public void sign_withShareMdClientIpAddressTrue() { assertCorrectSignatureRequestMade("QUALIFIED"); - assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertTrue(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), + Assertions.assertTrue(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); assertCorrectSessionRequestMade(); @@ -183,9 +184,9 @@ public void sign_withShareMdClientIpAddressFalse() { assertCorrectSignatureRequestMade("QUALIFIED"); - assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); + Assertions.assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - assertFalse(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), + Assertions.assertFalse(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); assertCorrectSessionRequestMade(); @@ -488,11 +489,11 @@ public void sign_signatureMissingInResponse_shouldThrowException() { private void assertCorrectSignatureRequestMade(String expectedCertificateLevel) { assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - assertEquals("relying-party-uuid", connector.signatureSessionRequestUsed.getRelyingPartyUUID()); - assertEquals("relying-party-name", connector.signatureSessionRequestUsed.getRelyingPartyName()); - assertEquals(expectedCertificateLevel, connector.signatureSessionRequestUsed.getCertificateLevel()); - assertEquals("SHA256", connector.signatureSessionRequestUsed.getHashType()); - assertEquals("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ=", connector.signatureSessionRequestUsed.getHash()); + Assertions.assertEquals("relying-party-uuid", connector.signatureSessionRequestUsed.getRelyingPartyUUID()); + Assertions.assertEquals("relying-party-name", connector.signatureSessionRequestUsed.getRelyingPartyName()); + Assertions.assertEquals(expectedCertificateLevel, connector.signatureSessionRequestUsed.getCertificateLevel()); + Assertions.assertEquals("SHA256", connector.signatureSessionRequestUsed.getHashType()); + Assertions.assertEquals("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ=", connector.signatureSessionRequestUsed.getHash()); } private void assertCorrectSessionRequestMade() { diff --git a/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java rename to src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java index 7488221a..5f09148e 100644 --- a/src/test/java/ee/sk/smartid/SmartIdAuthenticationResponseTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java similarity index 86% rename from src/test/java/ee/sk/smartid/SmartIdClientTest.java rename to src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java index 2c33fa75..daecec2b 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -32,11 +32,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubSessionStatusWithState; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubSessionStatusWithState; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; @@ -78,16 +78,14 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SemanticsIdentifier.CountryCode; -import ee.sk.smartid.rest.dao.SemanticsIdentifier.IdentityType; -import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.SmartIdRestConnector; @WireMockTest(httpPort = 18089) -public class SmartIdClientTest { +class SmartIdClientTest { private SmartIdClient client; @@ -122,9 +120,9 @@ public void setUp() { } @Test - public void getCertificateAndSign_fullExample() { + void getCertificateAndSign_fullExample() { // Provide data bytes to be signed (Default hash type is SHA-512) - SignableData dataToSign = new SignableData("Hello World!".getBytes()); + var dataToSign = new SignableData("Hello World!".getBytes()); // Calculate verification code assertEquals("4664", dataToSign.calculateVerificationCode()); @@ -160,7 +158,7 @@ public void getCertificateAndSign_fullExample() { } @Test - public void getCertificateAndSign_withExistingHash() { + void getCertificateAndSign_withExistingHash() { SmartIdCertificate certificateResponse = client .getCertificate() .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) @@ -188,8 +186,8 @@ public void getCertificateAndSign_withExistingHash() { } @Test - public void getCertificateUsingSemanticsIdentifier() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + void getCertificateUsingSemanticsIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); SmartIdCertificate certificate = client .getCertificate() @@ -201,7 +199,7 @@ public void getCertificateUsingSemanticsIdentifier() { } @Test - public void getCertificateUsingDocumentNumber() { + void getCertificateUsingDocumentNumber() { stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); SmartIdCertificate certificate = client @@ -214,7 +212,7 @@ public void getCertificateUsingDocumentNumber() { } @Test - public void getCertificateWithNonce() { + void getCertificateWithNonce() { stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); SmartIdCertificate certificate = client @@ -228,7 +226,7 @@ public void getCertificateWithNonce() { } @Test - public void getCertificateWithManualSessionStatusRequesting() { + void getCertificateWithManualSessionStatusRequesting() { stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); CertificateRequestBuilder builder = client.getCertificate(); @@ -245,9 +243,9 @@ public void getCertificateWithManualSessionStatusRequesting() { } @Test - public void noTrustStoreOrTrustedCertificates_shouldThrowException() { + void noTrustStoreOrTrustedCertificates_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - SmartIdClient client = new SmartIdClient(); + var client = new SmartIdClient(); client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); client.setRelyingPartyName("BANK123"); client.setHostUrl("http://localhost:18089"); @@ -263,7 +261,7 @@ public void noTrustStoreOrTrustedCertificates_shouldThrowException() { } @Test - public void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); @@ -281,8 +279,8 @@ public void getCertificateWithManualSessionStatusRequesting_andCustomResponseSoc } @Test - public void sign_withDocumentNumber() { - SignableHash hashToSign = new SignableHash(); + void sign_withDocumentNumber() { + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); @@ -303,14 +301,14 @@ public void sign_withDocumentNumber() { } @Test - public void sign_withSemanticsIdentifier() { - SignableHash hashToSign = new SignableHash(); + void sign_withSemanticsIdentifier() { + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); assertEquals("1796", hashToSign.calculateVerificationCode()); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789"); SmartIdSignature signature = client .createSignature() @@ -327,8 +325,8 @@ public void sign_withSemanticsIdentifier() { } @Test - public void signWithNonce() { - SignableHash hashToSign = new SignableHash(); + void signWithNonce() { + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); @@ -350,8 +348,8 @@ public void signWithNonce() { } @Test - public void signWithManualSessionStatusRequesting() { - SignableHash hashToSign = new SignableHash(); + void signWithManualSessionStatusRequesting() { + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); @@ -376,8 +374,8 @@ public void signWithManualSessionStatusRequesting() { } @Test - public void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - SignableHash hashToSign = new SignableHash(); + void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); @@ -404,7 +402,7 @@ public void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout } @Test - public void getCertificate_whenUserAccountNotFound_shouldThrowException() { + void getCertificate_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); makeGetCertificateRequest(); @@ -412,7 +410,7 @@ public void getCertificate_whenUserAccountNotFound_shouldThrowException() { } @Test - public void sign_whenUserAccountNotFound_shouldThrowException() { + void sign_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); makeCreateSignatureRequest(); @@ -420,7 +418,7 @@ public void sign_whenUserAccountNotFound_shouldThrowException() { } @Test - public void getCertificate_whenUserCancels_shouldThrowException() { + void getCertificate_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUserRefusedGeneral.json"); makeGetCertificateRequest(); @@ -428,7 +426,7 @@ public void getCertificate_whenUserCancels_shouldThrowException() { } @Test - public void sign_whenUserCancels_shouldThrowException() { + void sign_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenUserRefusedGeneral.json"); makeCreateSignatureRequest(); @@ -436,7 +434,7 @@ public void sign_whenUserCancels_shouldThrowException() { } @Test - public void sign_whenTimeout_shouldThrowException() { + void sign_whenTimeout_shouldThrowException() { assertThrows(SessionTimeoutException.class, () -> { stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenTimeout.json"); makeCreateSignatureRequest(); @@ -444,7 +442,7 @@ public void sign_whenTimeout_shouldThrowException() { } @Test - public void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { + void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); @@ -453,7 +451,7 @@ public void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowExc } @Test - public void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { + void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); @@ -462,7 +460,7 @@ public void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() } @Test - public void getCertificate_whenDocumentUnusable_shouldThrowException() { + void getCertificate_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenDocumentUnusable.json"); makeGetCertificateRequest(); @@ -470,7 +468,7 @@ public void getCertificate_whenDocumentUnusable_shouldThrowException() { } @Test - public void getCertificate_whenUnknownErrorCode_shouldThrowException() { + void getCertificate_whenUnknownErrorCode_shouldThrowException() { assertThrows(UnprocessableSmartIdResponseException.class, () -> { stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUnknownErrorCode.json"); makeGetCertificateRequest(); @@ -478,7 +476,7 @@ public void getCertificate_whenUnknownErrorCode_shouldThrowException() { } @Test - public void sign_whenDocumentUnusable_shouldThrowException() { + void sign_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenDocumentUnusable.json"); makeCreateSignatureRequest(); @@ -486,7 +484,7 @@ public void sign_whenDocumentUnusable_shouldThrowException() { } @Test - public void getCertificate_whenRequestForbidden_shouldThrowException() { + void getCertificate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); makeGetCertificateRequest(); @@ -494,7 +492,7 @@ public void getCertificate_whenRequestForbidden_shouldThrowException() { } @Test - public void sign_whenRequestForbidden_shouldThrowException() { + void sign_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); makeCreateSignatureRequest(); @@ -502,7 +500,7 @@ public void sign_whenRequestForbidden_shouldThrowException() { } @Test - public void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { + void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 471); makeGetCertificateRequest(); @@ -510,7 +508,7 @@ public void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitabl } @Test - public void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { + void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 472); makeGetCertificateRequest(); @@ -518,7 +516,7 @@ public void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonSho } @Test - public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 480); makeCreateSignatureRequest(); @@ -526,7 +524,7 @@ public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { } @Test - public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { + void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 580); makeGetCertificateRequest(); @@ -534,7 +532,7 @@ public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void sign_whenSystemUnderMaintenance_shouldThrowException() { + void sign_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 580); makeCreateSignatureRequest(); @@ -542,7 +540,7 @@ public void sign_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void setPollingSleepTimeoutForSignatureCreation() { + void setPollingSleepTimeoutForSignatureCreation() { stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); @@ -552,7 +550,7 @@ public void setPollingSleepTimeoutForSignatureCreation() { } @Test - public void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { + void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); SmartIdSignature signature = createSignature(); @@ -561,7 +559,7 @@ public void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { } @Test - public void createSignatureAndGetDeviceIpAddress() { + void createSignatureAndGetDeviceIpAddress() { stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); SmartIdSignature signature = createSignature(); @@ -571,7 +569,7 @@ public void createSignatureAndGetDeviceIpAddress() { } @Test - public void setPollingSleepTimeoutForCertificateChoice() { + void setPollingSleepTimeoutForCertificateChoice() { stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); @@ -583,7 +581,7 @@ public void setPollingSleepTimeoutForCertificateChoice() { } @Test - public void setSessionStatusResponseSocketTimeout() { + void setSessionStatusResponseSocketTimeout() { client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); SmartIdSignature signature = createSignature(); assertNotNull(signature); @@ -591,10 +589,10 @@ public void setSessionStatusResponseSocketTimeout() { } @Test - public void authenticateUsingDocumentNumber() { + void authenticateUsingDocumentNumber() { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -616,8 +614,8 @@ public void authenticateUsingDocumentNumber() { } @Test - public void authenticate_usingSemanticsIdentifier() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + void authenticate_usingSemanticsIdentifier() { + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -638,11 +636,11 @@ public void authenticate_usingSemanticsIdentifier() { } @Test - public void authenticateWithNonce() { + void authenticateWithNonce() { stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -664,10 +662,10 @@ public void authenticateWithNonce() { } @Test - public void authenticateWithManualSessionStatusRequesting() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); + void authenticateWithManualSessionStatusRequesting() { + var semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111"); - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -692,10 +690,10 @@ public void authenticateWithManualSessionStatusRequesting() { } @Test - public void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111"); + void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { + var semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111"); - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -721,7 +719,7 @@ public void authenticateWithManualSessionStatusRequesting_andCustomResponseSocke } @Test - public void authenticate_whenUserAccountNotFound_shouldThrowException() { + void authenticate_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); makeAuthenticationRequest(); @@ -729,7 +727,7 @@ public void authenticate_whenUserAccountNotFound_shouldThrowException() { } @Test - public void authenticate_whenUserCancels_shouldThrowException() { + void authenticate_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenUserRefusedGeneral.json"); @@ -738,7 +736,7 @@ public void authenticate_whenUserCancels_shouldThrowException() { } @Test - public void authenticate_whenTimeout_shouldThrowException() { + void authenticate_whenTimeout_shouldThrowException() { assertThrows(SessionTimeoutException.class, () -> { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenTimeout.json"); @@ -747,7 +745,7 @@ public void authenticate_whenTimeout_shouldThrowException() { } @Test - public void authenticate_whenDocumentUnusable_shouldThrowException() { + void authenticate_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenDocumentUnusable.json"); @@ -756,7 +754,7 @@ public void authenticate_whenDocumentUnusable_shouldThrowException() { } @Test - public void authenticate_whenRequestForbidden_shouldThrowException() { + void authenticate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); makeAuthenticationRequest(); @@ -764,7 +762,7 @@ public void authenticate_whenRequestForbidden_shouldThrowException() { } @Test - public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 480); makeAuthenticationRequest(); @@ -772,7 +770,7 @@ public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowExcep } @Test - public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { + void authenticate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 580); makeAuthenticationRequest(); @@ -780,7 +778,7 @@ public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void setPollingSleepTimeoutForAuthentication() { + void setPollingSleepTimeoutForAuthentication() { stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); @@ -791,7 +789,7 @@ public void setPollingSleepTimeoutForAuthentication() { @Test - public void getDeviceIpAddress_ipAddressNotPresent() { + void getDeviceIpAddress_ipAddressNotPresent() { stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); @@ -800,7 +798,7 @@ public void getDeviceIpAddress_ipAddressNotPresent() { } @Test - public void getDeviceIpAddress_ipAddressReturned() { + void getDeviceIpAddress_ipAddressReturned() { stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); @@ -809,7 +807,7 @@ public void getDeviceIpAddress_ipAddressReturned() { } @Test - public void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { + void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); String headerName = "custom-header"; @@ -826,7 +824,7 @@ public void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomH } @Test - public void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() { + void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() { String headerName = "custom-header"; String headerValue = "Hello?!"; @@ -841,7 +839,7 @@ public void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() } @Test - public void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCustomHeader() { + void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCustomHeader() { String headerName = "custom-header"; String headerValue = "Man, come on.."; @@ -856,13 +854,13 @@ public void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCust } @Test - public void verifySmartIdConnector_whenConnectorIsNotProvided() { + void verifySmartIdConnector_whenConnectorIsNotProvided() { SmartIdConnector smartIdConnector = client.getSmartIdConnector(); assertInstanceOf(SmartIdRestConnector.class, smartIdConnector); } @Test - public void verifySmartIdConnector_whenConnectorIsProvided() { + void verifySmartIdConnector_whenConnectorIsProvided() { final String mock = "MOCK"; SessionStatus status = mock(SessionStatus.class); when(status.getState()).thenReturn(mock); @@ -873,7 +871,7 @@ public void verifySmartIdConnector_whenConnectorIsProvided() { } @Test - public void getCertificate_noIdentifierGiven() { + void getCertificate_noIdentifierGiven() { assertThrows(SmartIdClientException.class, () -> client .getCertificate() @@ -883,10 +881,10 @@ public void getCertificate_noIdentifierGiven() { } @Test - public void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { + void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { SmartIdCertificate cer = client .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) .withCertificateLevel("ADVANCED") .fetch(); @@ -894,11 +892,11 @@ public void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCe } @Test - public void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldReturnValidCertificate() { + void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldReturnValidCertificate() { SmartIdCertificate cer = client .getCertificate() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) .withCertificateLevel("ADVANCED") .fetch(); @@ -906,11 +904,11 @@ public void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldRetur } @Test - public void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { + void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { SmartIdCertificate cer = client .getCertificate() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) .withCertificateLevel("ADVANCED") .fetch(); @@ -918,16 +916,16 @@ public void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCe } @Test - public void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); SmartIdAuthenticationResponse authResponse = client .createAuthentication() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) .withCertificateLevel("ADVANCED") .withAuthenticationHash(authenticationHash) .withAllowedInteractionsOrder(asList( @@ -940,16 +938,16 @@ public void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSucc } @Test - public void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); SmartIdAuthenticationResponse authResponse = client .createAuthentication() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) .withCertificateLevel("ADVANCED") .withAuthenticationHash(authenticationHash) .withAllowedInteractionsOrder(asList( @@ -962,16 +960,16 @@ public void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSucc } @Test - public void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { + void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); SmartIdAuthenticationResponse authResponse = client .createAuthentication() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) .withCertificateLevel("ADVANCED") .withAuthenticationHash(authenticationHash) .withAllowedInteractionsOrder(asList( @@ -984,16 +982,16 @@ public void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSucc } @Test - public void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - SignableHash signableHash = new SignableHash(); + var signableHash = new SignableHash(); signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); signableHash.setHashType(HashType.SHA256); SmartIdSignature signResponse = client .createSignature() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) .withCertificateLevel("ADVANCED") .withSignableHash(signableHash) .withAllowedInteractionsOrder(asList( @@ -1006,16 +1004,16 @@ public void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfu } @Test - public void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - SignableHash signableHash = new SignableHash(); + var signableHash = new SignableHash(); signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); signableHash.setHashType(HashType.SHA256); SmartIdSignature signResponse = client .createSignature() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.PAS, CountryCode.EE, "987654321012")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) .withCertificateLevel("ADVANCED") .withSignableHash(signableHash) .withAllowedInteractionsOrder(asList( @@ -1028,16 +1026,16 @@ public void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfu } @Test - public void getSignatureByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { + void getSignatureByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - SignableHash signableHash = new SignableHash(); + var signableHash = new SignableHash(); signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); signableHash.setHashType(HashType.SHA256); SmartIdSignature signResponse = client .createSignature() .withSemanticsIdentifier( - new SemanticsIdentifier(IdentityType.IDC, CountryCode.EE, "AA3456789")) + new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) .withCertificateLevel("ADVANCED") .withSignableHash(signableHash) .withAllowedInteractionsOrder(asList( @@ -1058,7 +1056,7 @@ private long measureSigningDuration() { } private SmartIdSignature createSignature() { - SignableHash hashToSign = new SignableHash(); + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); return client @@ -1082,7 +1080,7 @@ private long measureAuthenticationDuration() { } private SmartIdAuthenticationResponse createAuthentication() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -1113,13 +1111,13 @@ private long measureCertificateChoiceDuration() { private void makeGetCertificateRequest() { client .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(IdentityType.PNO, CountryCode.EE, "31111111111")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) .withCertificateLevel("ADVANCED") .fetch(); } private void makeCreateSignatureRequest() { - SignableHash hashToSign = new SignableHash(); + var hashToSign = new SignableHash(); hashToSign.setHashType(HashType.SHA256); hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); @@ -1136,7 +1134,7 @@ private void makeCreateSignatureRequest() { } private void makeAuthenticationRequest() { - AuthenticationHash authenticationHash = new AuthenticationHash(); + var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); authenticationHash.setHashType(HashType.SHA512); @@ -1153,7 +1151,7 @@ private void makeAuthenticationRequest() { } private ClientConfig getClientConfigWithCustomRequestHeaders(Map headers) { - ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); + var clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); clientConfig.register(new ClientRequestHeaderFilter(headers)); return clientConfig; } @@ -1185,7 +1183,7 @@ private void assertAuthenticationResponseValid(SmartIdAuthenticationResponse aut } private static String getAttribute(String name, ASN1ObjectIdentifier oid) { - X500Name x500name = new X500Name(name); + var x500name = new X500Name(name); RDN[] rdns = x500name.getRDNs(oid); return IETFUtils.valueToString(rdns[0].getFirst().getValue()); } diff --git a/src/test/java/ee/sk/SmartIdDemoCondition.java b/src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java similarity index 98% rename from src/test/java/ee/sk/SmartIdDemoCondition.java rename to src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java index ed0df541..fc687dc6 100644 --- a/src/test/java/ee/sk/SmartIdDemoCondition.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java @@ -1,4 +1,4 @@ -package ee.sk; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/test/java/ee/sk/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java similarity index 98% rename from src/test/java/ee/sk/SmartIdDemoIntegrationTest.java rename to src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java index e3ae6785..f1299e29 100644 --- a/src/test/java/ee/sk/SmartIdDemoIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java similarity index 99% rename from src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java rename to src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java index fb454a3b..394d1a37 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/SmartIdSignatureTest.java rename to src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java index 0557e112..63bd3058 100644 --- a/src/test/java/ee/sk/smartid/SmartIdSignatureTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -33,6 +33,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.v2.SmartIdSignature; public class SmartIdSignatureTest { diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java similarity index 93% rename from src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java rename to src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java index 3dc81eb9..ab22c516 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -33,6 +33,10 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.DigestCalculator; +import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.v2.VerificationCodeCalculator; + public class VerificationCodeCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java similarity index 92% rename from src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java rename to src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java index d72b2951..460612c3 100644 --- a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -26,7 +26,7 @@ * #L% */ -import static ee.sk.smartid.DummyData.createSessionEndResult; +import static ee.sk.smartid.v2.DummyData.createSessionEndResult; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -46,15 +46,17 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.rest.dao.CertificateRequest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SessionStatusRequest; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.rest.SessionStatusPoller; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SessionStatusRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; public class SessionStatusPollerTest { diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java new file mode 100644 index 00000000..c1567833 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java @@ -0,0 +1,116 @@ +package ee.sk.smartid.v2.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; + +public class SmartIdConnectorSpy implements SmartIdConnector { + + public SessionStatus sessionStatusToRespond; + public CertificateChoiceResponse certificateChoiceToRespond; + public SignatureSessionResponse signatureSessionResponseToRespond; + public AuthenticationSessionResponse authenticationSessionResponseToRespond; + + public String sessionIdUsed; + public SemanticsIdentifier semanticsIdentifierUsed; + public String documentNumberUsed; + public CertificateRequest certificateRequestUsed; + public SignatureSessionRequest signatureSessionRequestUsed; + public AuthenticationSessionRequest authenticationSessionRequestUsed; + + + @Override + public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { + sessionIdUsed = sessionId; + return sessionStatusToRespond; + } + + @Override + public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { + documentNumberUsed = documentNumber; + certificateRequestUsed = request; + return certificateChoiceToRespond; + } + + @Override + public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, CertificateRequest request) { + semanticsIdentifierUsed = identifier; + certificateRequestUsed = request; + return certificateChoiceToRespond; + } + + @Override + public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { + documentNumberUsed = documentNumber; + signatureSessionRequestUsed = request; + return signatureSessionResponseToRespond; + } + + @Override + public SignatureSessionResponse sign(SemanticsIdentifier identifier, SignatureSessionRequest request) { + semanticsIdentifierUsed = identifier; + signatureSessionRequestUsed = request; + return signatureSessionResponseToRespond; + } + + @Override + public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { + documentNumberUsed = documentNumber; + authenticationSessionRequestUsed = request; + return authenticationSessionResponseToRespond; + } + + @Override + public AuthenticationSessionResponse authenticate(SemanticsIdentifier identifier, AuthenticationSessionRequest request) { + semanticsIdentifierUsed = identifier; + authenticationSessionRequestUsed = request; + return authenticationSessionResponseToRespond; + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + + } + + @Override + public void setSslContext(SSLContext sslContext) { + + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java similarity index 85% rename from src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java rename to src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java index e48794e5..4f2b6b73 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -33,12 +33,12 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubBadRequestResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubUnauthorizedResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubBadRequestResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubUnauthorizedResponse; import static java.util.Arrays.asList; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,24 +57,26 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.ClientRequestHeaderFilter; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.SmartIdRestConnector; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.ClientRequestHeaderFilter; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.rest.dao.CertificateRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; @WireMockTest(httpPort = 18089) -public class SmartIdRestConnectorTest { +class SmartIdRestConnectorTest { private SmartIdConnector connector; @@ -84,7 +86,7 @@ public void setUp() { } @Test - public void getNotExistingSessionStatus() { + void getNotExistingSessionStatus() { assertThrows(SessionNotFoundException.class, () -> { stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); @@ -92,14 +94,14 @@ public void getNotExistingSessionStatus() { } @Test - public void getRunningSessionStatus() { + void getRunningSessionStatus() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunning.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); } @Test - public void getRunningSessionStatus_withIgnoredProperties() { + void getRunningSessionStatus_withIgnoredProperties() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunningWithIgnoredProperties.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); @@ -110,7 +112,7 @@ public void getRunningSessionStatus_withIgnoredProperties() { } @Test - public void getSessionStatus_forSuccessfulCertificateRequest() { + void getSessionStatus_forSuccessfulCertificateRequest() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulCertificateRequest.json"); assertSuccessfulResponse(sessionStatus); assertNotNull(sessionStatus.getCert()); @@ -119,7 +121,7 @@ public void getSessionStatus_forSuccessfulCertificateRequest() { } @Test - public void getSessionStatus_forSuccessfulSigningRequest() { + void getSessionStatus_forSuccessfulSigningRequest() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); assertSuccessfulResponse(sessionStatus); assertNotNull(sessionStatus.getSignature()); @@ -128,7 +130,7 @@ public void getSessionStatus_forSuccessfulSigningRequest() { } @Test - public void getSessionStatus_hasUserAgentHeader() { + void getSessionStatus_hasUserAgentHeader() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); assertSuccessfulResponse(sessionStatus); @@ -138,61 +140,61 @@ public void getSessionStatus_hasUserAgentHeader() { } @Test - public void getSessionStatus_userHasRefused() { + void getSessionStatus_userHasRefused() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test - public void getSessionStatus_userHasRefusedConfirmationMessage() { + void getSessionStatus_userHasRefusedConfirmationMessage() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); } @Test - public void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { + void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); } @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { + void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); } @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { + void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test - public void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { + void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); } @Test - public void getSessionStatus_timeout() { + void getSessionStatus_timeout() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenTimeout.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); } @Test - public void getSessionStatus_userHasSelectedWrongVcCode() { + void getSessionStatus_userHasSelectedWrongVcCode() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); } @Test - public void getSessionStatus_whenDocumentUnusable() { + void getSessionStatus_whenDocumentUnusable() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenDocumentUnusable.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); } @Test - public void getSessionStatus_withTimeoutParameter() { + void getSessionStatus_withTimeoutParameter() { stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); @@ -201,7 +203,7 @@ public void getSessionStatus_withTimeoutParameter() { } @Test - public void getCertificate_usingDocumentNumber() { + void getCertificate_usingDocumentNumber() { stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); CertificateRequest request = createDummyCertificateRequest(); CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); @@ -210,7 +212,7 @@ public void getCertificate_usingDocumentNumber() { } @Test - public void getCertificate_usingSemanticsIdentifier() { + void getCertificate_usingSemanticsIdentifier() { stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PASKZ-987654321012"); @@ -222,7 +224,7 @@ public void getCertificate_usingSemanticsIdentifier() { } @Test - public void getCertificate_withNonce_usingDocumentNumber() { + void getCertificate_withNonce_usingDocumentNumber() { stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); CertificateRequest request = createDummyCertificateRequest(); request.setNonce("zstOt2umlc"); @@ -232,7 +234,7 @@ public void getCertificate_withNonce_usingDocumentNumber() { } @Test - public void getCertificate_withNonce_usingSemanticsIdentifier() { + void getCertificate_withNonce_usingSemanticsIdentifier() { stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, "CZ", "1234567890"); CertificateRequest request = createDummyCertificateRequest(); @@ -243,7 +245,7 @@ public void getCertificate_withNonce_usingSemanticsIdentifier() { } @Test - public void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { + void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); @@ -252,7 +254,7 @@ public void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { } @Test - public void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { + void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequest.json"); @@ -264,7 +266,7 @@ public void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { } @Test - public void getCertificate_withWrongAuthenticationParams_shuldThrowException() { + void getCertificate_withWrongAuthenticationParams_shuldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); @@ -273,7 +275,7 @@ public void getCertificate_withWrongAuthenticationParams_shuldThrowException() { } @Test - public void getCertificate_withWrongRequestParams_shouldThrowException() { + void getCertificate_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); @@ -282,7 +284,7 @@ public void getCertificate_withWrongRequestParams_shouldThrowException() { } @Test - public void getCertificate_whenRequestForbidden_shouldThrowException() { + void getCertificate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); @@ -291,7 +293,7 @@ public void getCertificate_whenRequestForbidden_shouldThrowException() { } @Test - public void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 480); CertificateRequest request = createDummyCertificateRequest(); @@ -300,7 +302,7 @@ public void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowExc } @Test - public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { + void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 580); CertificateRequest request = createDummyCertificateRequest(); @@ -309,7 +311,7 @@ public void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void sign_usingDocumentNumber() { + void sign_usingDocumentNumber() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); SignatureSessionResponse response = connector.sign("PNOEE-123456", request); @@ -318,7 +320,7 @@ public void sign_usingDocumentNumber() { } @Test - public void sign_hasUserAgentHeader() { + void sign_hasUserAgentHeader() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); SignatureSessionResponse response = connector.sign("PNOEE-123456", createDummySignatureSessionRequest()); assertNotNull(response); @@ -329,7 +331,7 @@ public void sign_hasUserAgentHeader() { } @Test - public void sign_withNonce_usingDocumentNumber() { + void sign_withNonce_usingDocumentNumber() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); request.setNonce("zstOt2umlc"); @@ -339,7 +341,7 @@ public void sign_withNonce_usingDocumentNumber() { } @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { + void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -353,7 +355,7 @@ public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDi } @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { + void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_noFallback.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -366,7 +368,7 @@ public void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() } @Test - public void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { + void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -380,7 +382,7 @@ public void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackT } @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { + void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -394,7 +396,7 @@ public void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVe } @Test - public void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { + void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -408,7 +410,7 @@ public void sign_withAllowedInteractionsOrder_confirmationMessageAndVerification } @Test - public void sign_whenDocumentNumberNotFound_shouldThrowException() { + void sign_whenDocumentNumberNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -417,7 +419,7 @@ public void sign_whenDocumentNumberNotFound_shouldThrowException() { } @Test - public void sign_withWrongAuthenticationParams_shouldThrowException() { + void sign_withWrongAuthenticationParams_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubUnauthorizedResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -426,7 +428,7 @@ public void sign_withWrongAuthenticationParams_shouldThrowException() { } @Test - public void sign_withWrongRequestParams_shouldThrowException() { + void sign_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubBadRequestResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -435,7 +437,7 @@ public void sign_withWrongRequestParams_shouldThrowException() { } @Test - public void sign_whenRequestForbidden_shouldThrowException() { + void sign_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -444,7 +446,7 @@ public void sign_whenRequestForbidden_shouldThrowException() { } @Test - public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 480); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -453,7 +455,7 @@ public void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { } @Test - public void sign_whenSystemUnderMaintenance_shouldThrowException() { + void sign_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 580); SignatureSessionRequest request = createDummySignatureSessionRequest(); @@ -462,7 +464,7 @@ public void sign_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void authenticate_usingDocumentNumber() { + void authenticate_usingDocumentNumber() { stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); @@ -471,7 +473,7 @@ public void authenticate_usingDocumentNumber() { } @Test - public void authenticate_usingSemanticsIdentifier() { + void authenticate_usingSemanticsIdentifier() { stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, "KZ", "987654321012"); @@ -483,7 +485,7 @@ public void authenticate_usingSemanticsIdentifier() { } @Test - public void authenticate_withNonce_usingDocumentNumber() { + void authenticate_withNonce_usingDocumentNumber() { stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setNonce("g9rp4kjca3"); @@ -493,7 +495,7 @@ public void authenticate_withNonce_usingDocumentNumber() { } @Test - public void authenticate_withNonce_usingSemanticsIdentifier() { + void authenticate_withNonce_usingSemanticsIdentifier() { stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "48308230504"); @@ -507,7 +509,7 @@ public void authenticate_withNonce_usingSemanticsIdentifier() { @Test - public void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { + void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOLT-48010010101"); @@ -521,7 +523,7 @@ public void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() } @Test - public void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { + void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); @@ -532,7 +534,7 @@ public void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { } @Test - public void authenticate_hasUserAgentHeader() { + void authenticate_hasUserAgentHeader() { stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); @@ -545,7 +547,7 @@ public void authenticate_hasUserAgentHeader() { } @Test - public void authenticate_whenDocumentNumberNotFound_shouldThrowException() { + void authenticate_whenDocumentNumberNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -554,7 +556,7 @@ public void authenticate_whenDocumentNumberNotFound_shouldThrowException() { } @Test - public void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { + void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "requests/authenticationSessionRequest.json"); @@ -566,7 +568,7 @@ public void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() } @Test - public void authenticate_withWrongAuthenticationParams_shuldThrowException() { + void authenticate_withWrongAuthenticationParams_shuldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -575,7 +577,7 @@ public void authenticate_withWrongAuthenticationParams_shuldThrowException() { } @Test - public void authenticate_withWrongRequestParams_shouldThrowException() { + void authenticate_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubBadRequestResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -584,7 +586,7 @@ public void authenticate_withWrongRequestParams_shouldThrowException() { } @Test - public void authenticate_whenRequestForbidden_shouldThrowException() { + void authenticate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { stubForbiddenResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -593,7 +595,7 @@ public void authenticate_whenRequestForbidden_shouldThrowException() { } @Test - public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { + void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 480); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -602,7 +604,7 @@ public void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowExcep } @Test - public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { + void authenticate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 580); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); @@ -611,7 +613,7 @@ public void authenticate_whenSystemUnderMaintenance_shouldThrowException() { } @Test - public void verifyCustomRequestHeaderPresent_whenAuthenticating() { + void verifyCustomRequestHeaderPresent_whenAuthenticating() { String headerName = "custom-header"; String headerValue = "Auth"; @@ -627,7 +629,7 @@ public void verifyCustomRequestHeaderPresent_whenAuthenticating() { } @Test - public void verifyCustomRequestHeaderPresent_whenSigning() { + void verifyCustomRequestHeaderPresent_whenSigning() { String headerName = "custom-header"; String headerValue = "Sign"; @@ -643,7 +645,7 @@ public void verifyCustomRequestHeaderPresent_whenSigning() { } @Test - public void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { + void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { String headerName = "custom-header"; String headerValue = "Cert choice"; @@ -659,7 +661,7 @@ public void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { } @Test - public void getCertificate_hasUserAgentHeader() { + void getCertificate_hasUserAgentHeader() { connector = new SmartIdRestConnector("http://localhost:18089"); stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); connector.getCertificate("PNOEE-123456", createDummyCertificateRequest()); @@ -670,7 +672,7 @@ public void getCertificate_hasUserAgentHeader() { } @Test - public void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { + void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { String headerName = "custom-header"; String headerValue = "Session status"; @@ -685,7 +687,7 @@ public void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { } private ClientConfig getClientConfigWithCustomRequestHeader(Map headers) { - ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); + var clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); clientConfig.register(new ClientRequestHeaderFilter(headers)); return clientConfig; } @@ -708,7 +710,7 @@ private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { } private CertificateRequest createDummyCertificateRequest() { - CertificateRequest request = new CertificateRequest(); + var request = new CertificateRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); request.setCertificateLevel("ADVANCED"); @@ -716,7 +718,7 @@ private CertificateRequest createDummyCertificateRequest() { } private SignatureSessionRequest createDummySignatureSessionRequest() { - SignatureSessionRequest request = new SignatureSessionRequest(); + var request = new SignatureSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); request.setCertificateLevel("ADVANCED"); @@ -730,7 +732,7 @@ private SignatureSessionRequest createDummySignatureSessionRequest() { } private AuthenticationSessionRequest createDummyAuthenticationSessionRequest() { - AuthenticationSessionRequest request = new AuthenticationSessionRequest(); + var request = new AuthenticationSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); request.setCertificateLevel("ADVANCED"); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java similarity index 94% rename from src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java rename to src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index d7632c52..ce099830 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest; +package ee.sk.smartid.v2.rest; /*- * #%L @@ -43,18 +43,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.SmartIdDemoIntegrationTest; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.rest.dao.CertificateRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.rest.dao.SignatureSessionResponse; +import ee.sk.smartid.v2.DigestCalculator; +import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.v2.rest.SmartIdConnector; +import ee.sk.smartid.v2.rest.SmartIdRestConnector; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; +import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.SessionStatus; +import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; @SmartIdDemoIntegrationTest public class SmartIdRestIntegrationTest { diff --git a/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java similarity index 95% rename from src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java rename to src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java index d4027dc7..97171e58 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L @@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; + public class SemanticsIdentifierTest { @Test diff --git a/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java rename to src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java index 52249497..7b8a5e59 100644 --- a/src/test/java/ee/sk/smartid/rest/dao/SignatureSessionRequestTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.v2.rest.dao; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java rename to src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java index bfdfee5e..ee4fa7bd 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid.v2.util; /*- * #%L @@ -48,7 +48,8 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import ee.sk.CertificateUtil; +import ee.sk.smartid.v2.CertificateUtil; +import ee.sk.smartid.v2.util.CertificateAttributeUtil; public class CertificateAttributeUtilTest { diff --git a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java rename to src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java index 36939763..10acef0b 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid.v2.util; /*- * #%L @@ -40,10 +40,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import ee.sk.CertificateUtil; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.v2.AuthenticationResponseValidator; +import ee.sk.smartid.v2.CertificateUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.v2.util.NationalIdentityNumberUtil; public class NationalIdentityNumberUtilTest { diff --git a/src/test/resources/demo_server_trusted_ssl_certs.jks b/src/test/resources/demo_server_trusted_ssl_certs.jks index 82d4918dbec9abd913b7c23bc83952880788a2ea..3090342a02f0ad796a71c68d90845b0bdca158d6 100644 GIT binary patch delta 759 zcmVKV{h${QdY%nCwqDlR*LcT`PS#*kG++v@?b#~aN809`X>st zS2$aJvQd+9l@4k_lv_0$`g{7j#l9pk{#NhA^L>}&6!jEM%cl_X7V4`a@G&A0VQG*i zx_H@_ALD&yt93+`bI7d!?_COtSwrEhvN{j*Nc`7pe>cHn+}=jk+eQ9;BmI!*Q3oDT zqhIYwMpS<+h{g01oR<|m1L&bS+-VFl!LJrqOq54Le4@yledg3nRIfsrsKErncbwKp zAqOmzPYe|x9|i+e9U}x7FcyFmP9jtw7?tMZZsrgA`neMA1{aSxlY$4K z0!K2FB?v+SM>3OX2v>iQ@o>v1muZ)Lbp62kCVNw>t<7cqx|{}_iq~30hwV*FR$r~b z#kB<>E?Lq4O7);B*sztY$u3Ge*QfSxh$;LqKO}^^!?$~r2Q06LYj!j3098`5S)Sy% z(DUEt5B}Zw#JLc1fe8d(&zGJqGIfs6npo0TF@84zfwE?8DH?yVW(jT4Zjki3`Y!<# z#avco0oi?rs5n!fi(N)-T*fDafz*DCjH8=U=f;TaH+(Al@KstkauC=66VFU@+p0G; zs40PIoOZn47<-V^%Fpj-1N{VAAr`U!>ObGyg7xe4S&}0SBr269Ov<5==G3>Whjg>? pZKIjn`PIFIK%W8!n6V5M78FE3KmR_fea8Sh77!|2;l@R3N|Xd8QkVb$ delta 43 ycmca-wpEAc-`jt085kItfS6^oDXXZM$fhMxhpwgHmNR|Awr1CHn-Z?OYw`ecLJ_Y3 From 5eb16e32e9badecc55d9a8cce95ebae29fcd61c2 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 18 Oct 2024 12:04:14 +0300 Subject: [PATCH 03/57] SLIB-61 - update OWASP dependency (#87) * SLIB-61 - update OWASP dependency check and update Travis-CI to use NVD API key for OWASP check * SLIB-61 - fix NVD_key usage * SLIB-61 - fix NVD_key usage * SLIB-61 - make spotbugs check more relaxed * SLIB-61 - restore logging spotbugs results * SLIB-61 - change OWASP check to run on merge --- pom.xml | 5 ++++- travis.sh | 9 ++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 7fbba6ba..5e7ef86b 100644 --- a/pom.xml +++ b/pom.xml @@ -247,7 +247,7 @@ org.owasp dependency-check-maven - 8.2.1 + 10.0.4 true false @@ -265,6 +265,9 @@ com.github.spotbugs spotbugs-maven-plugin 4.8.6.4 + + false + diff --git a/travis.sh b/travis.sh index f0d0ba95..f06afcf6 100644 --- a/travis.sh +++ b/travis.sh @@ -7,14 +7,13 @@ echo "Is pull request: $TRAVIS_PULL_REQUEST" echo "Tag: $TRAVIS_TAG" echo "JDK version: $TRAVIS_JDK_VERSION" -if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$TRAVIS_JDK_VERSION" == "openjdk8" ]; then +if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$TRAVIS_JDK_VERSION" == "openjdk17" ]; then echo "Starting to publish" ./publish.sh echo "Finished" -elif [ "$TRAVIS_JDK_VERSION" == "openjdk8" ]; then - ./mvnw test - ./mvnw org.owasp:dependency-check-maven:check - ./mvnw spotbugs:check +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_JDK_VERSION" == "openjdk17" ]; then + ./mvnw -DnvdApiKey="$NVD_key" org.owasp:dependency-check-maven:check else ./mvnw test + ./mvnw spotbugs:check fi From add26cbfaaf1464fb2183df9b294410fcfa32258 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 25 Oct 2024 11:39:42 +0300 Subject: [PATCH 04/57] SLIB-52 - add initializing authentication session with dynamic link (#89) * SLIB-52 - add dynamic link authentication request and response with mandatory field validations * SLIB-52 - add handling for anonymous dynamic link authentication; refacto v2 test-data * SLIB-52 - add handling for initializing dynamic link authentication session with semantics identifier * SLIB-52 - add handling for initializing dynamic link authentication session with document number * SLIB-52 - remove redundant signatureProtocol check * SLIB-52 - fix invalid nonce length check * SLIB-52 - change setting value to signatureProtocolParameters; add generating random challenge * SLIB-52 - improve code style * SLIB-52 - add documentation for dynamic link authentication * SLIB-52 - add license headers to the new files * SLIB-52 - update readme and changelog --- CHANGELOG.md | 1 + README.md | 123 +++- .../v3/AuthenticationCertificateLevel.java | 31 + ...namicLinkAuthenticationSessionRequest.java | 128 +++++ ...nkAuthenticationSessionRequestBuilder.java | 339 +++++++++++ ...amicLinkAuthenticationSessionResponse.java | 58 ++ .../ee/sk/smartid/v3/RandomChallenge.java | 74 +++ .../ee/sk/smartid/v3/SignatureAlgorithm.java | 44 ++ .../ee/sk/smartid/v3/SignatureProtocol.java | 31 + .../v3/SignatureProtocolParameters.java | 51 ++ .../java/ee/sk/smartid/v3/SmartIdClient.java | 4 + .../sk/smartid/v3/rest/SmartIdConnector.java | 31 +- .../smartid/v3/rest/SmartIdRestConnector.java | 52 +- .../java/ee/sk/smartid/{v2 => }/FileUtil.java | 2 +- .../{v2 => }/SmartIdDemoCondition.java | 2 +- .../{v2 => }/SmartIdDemoIntegrationTest.java | 2 +- .../{v2 => }/SmartIdRestServiceStubs.java | 2 +- .../ee/sk/smartid/integration/ReadmeTest.java | 4 +- .../integration/SmartIdIntegrationTest.java | 4 +- ...ndpointSslVerificationIntegrationTest.java | 1 + .../ee/sk/smartid/v2/SmartIdClientTest.java | 146 ++--- .../v2/rest/SmartIdRestConnectorTest.java | 132 +++-- .../v2/rest/SmartIdRestIntegrationTest.java | 4 +- ...thenticationSessionRequestBuilderTest.java | 529 ++++++++++++++++++ .../ee/sk/smartid/v3/RandomChallengeTest.java | 68 +++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 109 ++++ .../v3/rest/SmartIdRestConnectorTest.java | 174 ++++++ .../v3/rest/SmartIdRestIntegrationTest.java | 95 ++++ .../authenticationSessionRequest.json | 0 ...authenticationSessionRequestWithNonce.json | 0 ...onRequestWithSingleAllowedInteraction.json | 0 .../requests/certificateChoiceRequest.json | 0 .../certificateChoiceRequestWithNonce.json | 0 .../requests/signatureSessionRequest.json | 0 .../signatureSessionRequestWithNonce.json | 0 ...reSessionRequestWithRequestProperties.json | 0 .../signatureSessionRequestWithSha512.json | 0 ...ice_fallbackTo_verificationCodeChoice.json | 0 ...nMessage_fallbackTo_displayTextAndPIN.json | 0 ...age_fallbackTo_verificationCodeChoice.json | 0 ...equest_confirmationMessage_noFallback.json | 0 ...deChoice_fallbackTo_displayTextAndPIN.json | 0 .../authenticationSessionResponse.json | 0 .../responses/certificateChoiceResponse.json | 0 ...tusForSuccessfulAuthenticationRequest.json | 0 ...henticationRequestWithDeviceIpAddress.json | 0 ...StatusForSuccessfulCertificateRequest.json | 0 ...sionStatusForSuccessfulSigningRequest.json | 0 ...sfulSigningRequestWithDeviceIpAddress.json | 0 .../responses/sessionStatusRunning.json | 0 ...ionStatusRunningWithIgnoredProperties.json | 0 .../sessionStatusWhenDocumentUnusable.json | 0 ...nRequiredInteractionNotSupportedByApp.json | 0 .../responses/sessionStatusWhenTimeout.json | 0 .../sessionStatusWhenUnknownErrorCode.json | 0 ...nStatusWhenUserHasSelectedWrongVcCode.json | 0 ...essionStatusWhenUserRefusedCertChoice.json | 0 ...tusWhenUserRefusedConfirmationMessage.json | 0 ...tionMessageWithVerificationCodeChoice.json | 0 ...tatusWhenUserRefusedDisplayTextAndPin.json | 0 .../sessionStatusWhenUserRefusedGeneral.json | 0 ...WhenUserRefusedVerificationCodeChoice.json | 0 .../responses/signatureSessionResponse.json | 0 ...c-link-authentication-session-request.json | 12 + ...-link-authentication-session-response.json | 5 + 65 files changed, 2103 insertions(+), 155 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java create mode 100644 src/main/java/ee/sk/smartid/v3/RandomChallenge.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignatureProtocol.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java rename src/test/java/ee/sk/smartid/{v2 => }/FileUtil.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/SmartIdDemoCondition.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/SmartIdDemoIntegrationTest.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/SmartIdRestServiceStubs.java (99%) create mode 100644 src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java rename src/test/resources/{ => v2}/requests/authenticationSessionRequest.json (100%) rename src/test/resources/{ => v2}/requests/authenticationSessionRequestWithNonce.json (100%) rename src/test/resources/{ => v2}/requests/authenticationSessionRequestWithSingleAllowedInteraction.json (100%) rename src/test/resources/{ => v2}/requests/certificateChoiceRequest.json (100%) rename src/test/resources/{ => v2}/requests/certificateChoiceRequestWithNonce.json (100%) rename src/test/resources/{ => v2}/requests/signatureSessionRequest.json (100%) rename src/test/resources/{ => v2}/requests/signatureSessionRequestWithNonce.json (100%) rename src/test/resources/{ => v2}/requests/signatureSessionRequestWithRequestProperties.json (100%) rename src/test/resources/{ => v2}/requests/signatureSessionRequestWithSha512.json (100%) rename src/test/resources/{ => v2}/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json (100%) rename src/test/resources/{ => v2}/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json (100%) rename src/test/resources/{ => v2}/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json (100%) rename src/test/resources/{ => v2}/requests/signingRequest_confirmationMessage_noFallback.json (100%) rename src/test/resources/{ => v2}/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json (100%) rename src/test/resources/{ => v2}/responses/authenticationSessionResponse.json (100%) rename src/test/resources/{ => v2}/responses/certificateChoiceResponse.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusForSuccessfulAuthenticationRequest.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusForSuccessfulCertificateRequest.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusForSuccessfulSigningRequest.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusRunning.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusRunningWithIgnoredProperties.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenDocumentUnusable.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenTimeout.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUnknownErrorCode.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedCertChoice.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedConfirmationMessage.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedGeneral.json (100%) rename src/test/resources/{ => v2}/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json (100%) rename src/test/resources/{ => v2}/responses/signatureSessionResponse.json (100%) create mode 100644 src/test/resources/v3/requests/dynamic-link-authentication-session-request.json create mode 100644 src/test/resources/v3/responses/dynamic-link-authentication-session-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bc4dbd..7a35603b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support for Smart-ID API v3.0 has been added under the ee.sk.smartid.v3 package. +- Added handling for dynamic-link authentication session requests. View V3 section in README.md for more information. ### Changed - Existing code for Smart-ID API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. diff --git a/README.md b/README.md index 61639c68..17ec46b1 100644 --- a/README.md +++ b/README.md @@ -661,4 +661,125 @@ var client = new SmartIdClient(); client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -``` \ No newline at end of file + client.setTrustStore(trustStore); +``` + +## Dynamic Link flows + +### Examples of performing authentication + +#### Initiating authentication session with semantics identifier + +More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "30303039914"); // identifier (according to country and identity type reference) + +// For security reasons a new random challenge must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + // Smart-ID app will display verification code to the user and user must insert PIN1 + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder( + Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + +#### Initiating authentication session with document number + +If you already know the documentNumber you can use this for (re-)authentication. + +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication + +// For security reasons a new hash value must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + // Smart-ID app will display verification code to the user and user must insert PIN1 + .withAllowedInteractionsOrder( + Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + +### Initiating anonymous authentication session + +Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. +RP can learn the user's identity only after the user has authenticated themselves. + +```java +// For security reasons a new hash value must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + // before the user can enter PIN. If user selects wrong verification code then the operation will fail. + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + + +### Generating QR-code or dynamic link +Todo: will be implemented in task SLIB-55 \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java new file mode 100644 index 00000000..c34c4041 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java @@ -0,0 +1,31 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public enum AuthenticationCertificateLevel { + ADVANCED, QUALIFIED +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java new file mode 100644 index 00000000..45c514e1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java @@ -0,0 +1,128 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.RequestProperties; + +public class DynamicLinkAuthenticationSessionRequest implements Serializable { + + private String relyingPartyUUID; + + private String relyingPartyName; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String certificateLevel; + + private final SignatureProtocol signatureProtocol = SignatureProtocol.ACSP_V1; + + private SignatureProtocolParameters signatureProtocolParameters; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String nonce; + + private List allowedInteractionsOrder; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private RequestProperties requestProperties; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Set capabilities; + + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + public String getRelyingPartyName() { + return relyingPartyName; + } + + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public SignatureProtocol getSignatureProtocol() { + return signatureProtocol; + } + + public SignatureProtocolParameters getSignatureProtocolParameters() { + return signatureProtocolParameters; + } + + public void setSignatureProtocolParameters(SignatureProtocolParameters signatureProtocolParameters) { + this.signatureProtocolParameters = signatureProtocolParameters; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public List getAllowedInteractionsOrder() { + return allowedInteractionsOrder; + } + + public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + } + + public RequestProperties getRequestProperties() { + return requestProperties; + } + + public void setRequestProperties(RequestProperties requestProperties) { + this.requestProperties = requestProperties; + } + + public Set getCapabilities() { + return capabilities; + } + + public void setCapabilities(Set capabilities) { + this.capabilities = capabilities; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java new file mode 100644 index 00000000..816b1dd5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -0,0 +1,339 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import java.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +/** + * Class for building a dynamic link authentication session request + */ +public class DynamicLinkAuthenticationSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(DynamicLinkAuthenticationSessionRequestBuilder.class); + + private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = + Set.of(InteractionFlow.VERIFICATION_CODE_CHOICE, InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel; + private String randomChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; + private String nonce; + private List allowedInteractionsOrder; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + + /** + * Constructs a new DynamicLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DynamicLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the random challenge + *

        + * The provided random challenge must be a Base64 encoded string + * + * @param randomChallenge the signature protocol parameters + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withRandomChallenge(String randomChallenge) { + this.randomChallenge = randomChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the allowed interactions order + * + * @param allowedInteractionsOrder the allowed interactions order + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + return this; + } + + /** + * Sets whether to share the Mobile-ID client IP address + * + * @param shareMdClientIpAddress whether to share the Mobile-ID client IP address + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withSharedMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

        + * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

        + * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public DynamicLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

        + * There are 3 supported ways to start authentication session: + *

          + *
        • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
        • + *
        • with document number by using {@link #withDocumentNumber(String)}
        • + *
        • anonymously if semantics identifier and document number are not provided
        • + *
        + * + * @return init session response + */ + public DynamicLinkAuthenticationSessionResponse initAuthenticationSession() { + validateRequestParameters(); + DynamicLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + DynamicLinkAuthenticationSessionResponse dynamicLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(dynamicLinkAuthenticationSessionResponse); + return dynamicLinkAuthenticationSessionResponse; + } + + private DynamicLinkAuthenticationSessionResponse initAuthenticationSession(DynamicLinkAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null) { + return connector.initDynamicLinkAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initDynamicLinkAuthentication(authenticationRequest, documentNumber); + } else { + return connector.initAnonymousDynamicLinkAuthentication(authenticationRequest); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + validateSignatureParameters(); + validateNonce(); + validateAllowedInteractionOrder(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(randomChallenge)) { + logger.error("Parameter randomChallenge must be set"); + throw new SmartIdClientException("Parameter randomChallenge must be set"); + } + byte[] challenge = getDecodedRandomChallenge(); + if (challenge.length < 32 || challenge.length > 64) { + logger.error("Size of parameter randomChallenge must be between 32 and 64 bytes"); + throw new SmartIdClientException("Size of parameter randomChallenge must be between 32 and 64 bytes"); + } + if (signatureAlgorithm == null) { + logger.error("Parameter signatureAlgorithm must be set"); + throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); + } + } + + private byte[] getDecodedRandomChallenge() { + Base64.Decoder decoder = Base64.getDecoder(); + try { + return decoder.decode(randomChallenge); + } catch (IllegalArgumentException e) { + logger.error("Parameter randomChallenge is not a valid Base64 encoded string"); + throw new SmartIdClientException("Parameter randomChallenge is not a valid Base64 encoded string"); + } + } + + private void validateNonce() { + if (nonce == null) { + return; + } + if (nonce.isEmpty()) { + logger.error("Parameter nonce value has to be at least 1 character long"); + throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); + } + if (nonce.length() > 30) { + logger.error("Nonce cannot be longer that 30 chars"); + throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); + } + } + + private void validateAllowedInteractionOrder() { + if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + logger.error("Parameter allowedInteractionsOrder must be set"); + throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); + } + Optional notSupportedInteraction = allowedInteractionsOrder.stream() + .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) + .findFirst(); + if (notSupportedInteraction.isPresent()) { + logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); + throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); + } + allowedInteractionsOrder.forEach(Interaction::validate); + } + + private DynamicLinkAuthenticationSessionRequest createAuthenticationRequest() { + var request = new DynamicLinkAuthenticationSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + var signatureProtocolParameters = new SignatureProtocolParameters(); + signatureProtocolParameters.setRandomChallenge(randomChallenge); + signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setNonce(nonce); + request.setAllowedInteractionsOrder(allowedInteractionsOrder); + + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + request.setRequestProperties(requestProperties); + } + request.setCapabilities(capabilities); + return request; + } + + private void validateResponseParameters(DynamicLinkAuthenticationSessionResponse dynamicLinkAuthenticationSessionResponse) { + if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new SmartIdClientException("Session ID is missing from the response"); + } + + if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionToken())) { + logger.error("Session token is missing from the response"); + throw new SmartIdClientException("Session token is missing from the response"); + } + + if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionSecret())) { + logger.error("Session secret is missing from the response"); + throw new SmartIdClientException("Session secret is missing from the response"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java new file mode 100644 index 00000000..2180dba3 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java @@ -0,0 +1,58 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public class DynamicLinkAuthenticationSessionResponse { + + private String sessionID; + private String sessionToken; + private String sessionSecret; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public String getSessionToken() { + return sessionToken; + } + + public void setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + } + + public String getSessionSecret() { + return sessionSecret; + } + + public void setSessionSecret(String sessionSecret) { + this.sessionSecret = sessionSecret; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/RandomChallenge.java b/src/main/java/ee/sk/smartid/v3/RandomChallenge.java new file mode 100644 index 00000000..128816fb --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/RandomChallenge.java @@ -0,0 +1,74 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; + +import org.bouncycastle.util.encoders.Base64; + +/** + * Utility class for generating random challenges in Base64 format + */ +public class RandomChallenge { + + private static final int MAX_LENGTH = 64; + private static final int MIN_LENGTH = 32; + + private RandomChallenge() { + } + + /** + * Generates a random challenge with max length of 64 bytes + * + * @return random challenge in Base64 format + */ + public static String generate() { + byte[] randBytes = new byte[MAX_LENGTH]; + new SecureRandom().nextBytes(randBytes); + return Base64.toBase64String(randBytes); + } + + /** + * Generates a random challenge with specified length + * + * @param length length of the challenge + * @return random challenge in Base64 format + */ + public static String generate(int length) { + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new IllegalArgumentException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); + } + byte[] randBytes = getRandomBytes(length); + return Base64.toBase64String(randBytes); + } + + private static byte[] getRandomBytes(int length) { + byte[] randBytes = new byte[length]; + new SecureRandom().nextBytes(randBytes); + return randBytes; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java new file mode 100644 index 00000000..2446c7f4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java @@ -0,0 +1,44 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public enum SignatureAlgorithm { + + SHA256WITHRSA("sha256WithRSAEncryption"), + SHA384WITHRSA("sha384WithRSAEncryption"), + SHA512WITHRSA("sha512WithRSAEncryption"); + + private final String algorithmName; + + SignatureAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + public String getAlgorithmName() { + return algorithmName; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java b/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java new file mode 100644 index 00000000..a8027330 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java @@ -0,0 +1,31 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public enum SignatureProtocol { + ACSP_V1 +} diff --git a/src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java new file mode 100644 index 00000000..9c1b7424 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java @@ -0,0 +1,51 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class SignatureProtocolParameters implements Serializable { + + private String randomChallenge; + private String signatureAlgorithm; + + public String getRandomChallenge() { + return randomChallenge; + } + + public void setRandomChallenge(String randomChallenge) { + this.randomChallenge = randomChallenge; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 1d163025..9b4877cb 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -63,6 +63,10 @@ public class SmartIdClient { private SmartIdConnector connector; private SSLContext trustSslContext; + public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentication() { + return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()); + } + /** * Sets the UUID of the relying party *

        diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index 0439a832..14c1c12d 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -31,8 +31,9 @@ import javax.net.ssl.SSLContext; -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; public interface SmartIdConnector extends Serializable { @@ -50,4 +51,30 @@ public interface SmartIdConnector extends Serializable { * @param sslContext The SSL context */ void setSslContext(SSLContext sslContext); + + /** + * Create anonymous authentication session with dynamic link + * + * @param authenticationRequest The dynamic link authentication session request + * @return The dynamic link authentication session response + */ + DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest); + + /** + * Create authentication session with dynamic link using semantics identifier + * + * @param authenticationRequest The dynamic link authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The dynamic link authentication session response + */ + DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with dynamic link using document number + * + * @param authenticationRequest The dynamic link authentication session request + * @param documentNumber The document number + * @return The dynamic link authentication session response + */ + DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index 9c5159b7..ffe10e32 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -43,11 +43,16 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.LoggingFilter; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.ServerErrorException; @@ -62,12 +67,16 @@ public class SmartIdRestConnector implements SmartIdConnector { @Serial - private static final long serialVersionUID = 44L; + private static final long serialVersionUID = 2024_10_18; private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + private static final String ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH = "authentication/dynamic-link/anonymous"; + private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; + private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/dynamic-link/document"; + private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; @@ -200,4 +209,45 @@ public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusRespons public void setSslContext(SSLContext sslContext) { this.sslContext = sslContext; } + + @Override + public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest) { + logger.debug("Starting anonymous dynamic link authentication session"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); + } + + @Override + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + logger.debug("Starting dynamic link authentication session with semantics identifier"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); + } + + @Override + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { + logger.debug("Starting dynamic link authentication session with document number"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); + } + + private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI uri, DynamicLinkAuthenticationSessionRequest request) { + try { + return postRequest(uri, request, DynamicLinkAuthenticationSessionResponse.class); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException e) { + logger.warn("No permission to issue the request", e); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); + } + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v2/FileUtil.java b/src/test/java/ee/sk/smartid/FileUtil.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/FileUtil.java rename to src/test/java/ee/sk/smartid/FileUtil.java index fd307779..f1429492 100644 --- a/src/test/java/ee/sk/smartid/v2/FileUtil.java +++ b/src/test/java/ee/sk/smartid/FileUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java rename to src/test/java/ee/sk/smartid/SmartIdDemoCondition.java index fc687dc6..5bfbd684 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdDemoCondition.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java rename to src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java index f1299e29..0164ff42 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdDemoIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java similarity index 99% rename from src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java rename to src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 394d1a37..fb454a3b 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java index 9cabdc1a..2dcfc2e9 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java @@ -63,8 +63,8 @@ import ee.sk.smartid.v2.CertificateParser; import ee.sk.smartid.v2.HashType; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.FileUtil; -import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.SignableHash; import ee.sk.smartid.v2.SmartIdAuthenticationResponse; import ee.sk.smartid.v2.SmartIdCertificate; diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java index dd4262ca..708eb380 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java @@ -46,8 +46,8 @@ import ee.sk.smartid.v2.AuthenticationIdentity; import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.FileUtil; -import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.SignableData; import ee.sk.smartid.v2.SmartIdAuthenticationResponse; import ee.sk.smartid.v2.SmartIdCertificate; diff --git a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java index ef8f955e..34c15b29 100644 --- a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.integration.SmartIdIntegrationTest; import jakarta.ws.rs.ProcessingException; diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java index daecec2b..674ce6ab 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java @@ -32,11 +32,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubSessionStatusWithState; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubSessionStatusWithState; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; @@ -97,26 +97,26 @@ public void setUp() { client.setHostUrl("http://localhost:18089"); client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN\nMjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb\nMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h\ncnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS\nlaHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj\njgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt\n/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO\ndRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan\ngnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38\nyJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN\nRji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX\nMBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW\nMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v\nY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov\nL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3\nBglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu\nY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl\ncnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw\nDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K\ncbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E\noZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd\n0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g\ngw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW\nw+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu\nzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU\n9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1\nfM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2\nFJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg\nlWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t\nYv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu\nulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi\n/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3\ndgE=\n-----END CERTIFICATE-----\n"); - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithSha512.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequestWithSha512.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequestWithNonce.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/PNOEE-31111111111", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/PASEE-987654321012", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/IDCEE-AA3456789", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json"); + stubRequestWithResponse("/signature/etsi/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/etsi/PASEE-987654321012", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/etsi/IDCEE-AA3456789", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json"); - stubRequestWithResponse("/authentication/document/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PNOEE-31111111111", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PASEE-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/IDCEE-AA3456789", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-31111111111", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PNOEE-31111111111", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PASEE-987654321012", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/IDCEE-AA3456789", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/PASEE-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/IDCEE-AA3456789", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json"); + stubRequestWithResponse("/certificatechoice/etsi/PASEE-987654321012", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/IDCEE-AA3456789", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json"); } @Test @@ -200,7 +200,7 @@ void getCertificateUsingSemanticsIdentifier() { @Test void getCertificateUsingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); SmartIdCertificate certificate = client .getCertificate() @@ -213,7 +213,7 @@ void getCertificateUsingDocumentNumber() { @Test void getCertificateWithNonce() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); SmartIdCertificate certificate = client .getCertificate() @@ -227,7 +227,7 @@ void getCertificateWithNonce() { @Test void getCertificateWithManualSessionStatusRequesting() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); CertificateRequestBuilder builder = client.getCertificate(); String sessionId = builder @@ -262,7 +262,7 @@ void noTrustStoreOrTrustedCertificates_shouldThrowException() { @Test void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); CertificateRequestBuilder builder = client.getCertificate(); @@ -404,7 +404,7 @@ void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { @Test void getCertificate_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); + stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json"); makeGetCertificateRequest(); }); } @@ -412,7 +412,7 @@ void getCertificate_whenUserAccountNotFound_shouldThrowException() { @Test void sign_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); + stubNotFoundResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json"); makeCreateSignatureRequest(); }); } @@ -420,7 +420,7 @@ void sign_whenUserAccountNotFound_shouldThrowException() { @Test void getCertificate_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUserRefusedGeneral.json"); + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); makeGetCertificateRequest(); }); } @@ -428,7 +428,7 @@ void getCertificate_whenUserCancels_shouldThrowException() { @Test void sign_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenUserRefusedGeneral.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); makeCreateSignatureRequest(); }); } @@ -436,7 +436,7 @@ void sign_whenUserCancels_shouldThrowException() { @Test void sign_whenTimeout_shouldThrowException() { assertThrows(SessionTimeoutException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenTimeout.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenTimeout.json"); makeCreateSignatureRequest(); }); } @@ -444,8 +444,8 @@ void sign_whenTimeout_shouldThrowException() { @Test void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); makeAuthenticationRequest(); }); } @@ -453,8 +453,8 @@ void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException( @Test void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/signatureSessionResponse.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); makeAuthenticationRequest(); }); } @@ -462,7 +462,7 @@ void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { @Test void getCertificate_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenDocumentUnusable.json"); + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenDocumentUnusable.json"); makeGetCertificateRequest(); }); } @@ -470,7 +470,7 @@ void getCertificate_whenDocumentUnusable_shouldThrowException() { @Test void getCertificate_whenUnknownErrorCode_shouldThrowException() { assertThrows(UnprocessableSmartIdResponseException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusWhenUnknownErrorCode.json"); + stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenUnknownErrorCode.json"); makeGetCertificateRequest(); }); } @@ -478,7 +478,7 @@ void getCertificate_whenUnknownErrorCode_shouldThrowException() { @Test void sign_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusWhenDocumentUnusable.json"); + stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenDocumentUnusable.json"); makeCreateSignatureRequest(); }); } @@ -486,7 +486,7 @@ void sign_whenDocumentUnusable_shouldThrowException() { @Test void getCertificate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json"); + stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json"); makeGetCertificateRequest(); }); } @@ -494,7 +494,7 @@ void getCertificate_whenRequestForbidden_shouldThrowException() { @Test void sign_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json"); + stubForbiddenResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json"); makeCreateSignatureRequest(); }); } @@ -502,7 +502,7 @@ void sign_whenRequestForbidden_shouldThrowException() { @Test void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 471); + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 471); makeGetCertificateRequest(); }); } @@ -510,7 +510,7 @@ void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccoun @Test void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 472); + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 472); makeGetCertificateRequest(); }); } @@ -518,7 +518,7 @@ void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldView @Test void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 480); + stubErrorResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", 480); makeCreateSignatureRequest(); }); } @@ -526,7 +526,7 @@ void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { @Test void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "requests/certificateChoiceRequest.json", 580); + stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 580); makeGetCertificateRequest(); }); } @@ -534,15 +534,15 @@ void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { @Test void sign_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-31111111111", "requests/signatureSessionRequest.json", 580); + stubErrorResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", 580); makeCreateSignatureRequest(); }); } @Test void setPollingSleepTimeoutForSignatureCreation() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); long duration = measureSigningDuration(); assertTrue(duration > 2000L, "Duration is " + duration); @@ -551,8 +551,8 @@ void setPollingSleepTimeoutForSignatureCreation() { @Test void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); SmartIdSignature signature = createSignature(); assertThat(signature.getDeviceIpAddress(), is(nullValue())); @@ -560,8 +560,8 @@ void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { @Test void createSignatureAndGetDeviceIpAddress() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); SmartIdSignature signature = createSignature(); assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); @@ -570,10 +570,10 @@ void createSignatureAndGetDeviceIpAddress() { @Test void setPollingSleepTimeoutForCertificateChoice() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "responses/sessionStatusForSuccessfulCertificateRequest.json", "COMPLETE", STARTED); + stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json", "COMPLETE", STARTED); client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); long duration = measureCertificateChoiceDuration(); assertTrue(duration > 2000L, "Duration is " + duration); @@ -590,7 +590,7 @@ void setSessionStatusResponseSocketTimeout() { @Test void authenticateUsingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); var authenticationHash = new AuthenticationHash(); authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); @@ -637,7 +637,7 @@ void authenticate_usingSemanticsIdentifier() { @Test void authenticateWithNonce() { - stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); var authenticationHash = new AuthenticationHash(); @@ -721,7 +721,7 @@ void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeou @Test void authenticate_whenUserAccountNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); + stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json"); makeAuthenticationRequest(); }); } @@ -729,8 +729,8 @@ void authenticate_whenUserAccountNotFound_shouldThrowException() { @Test void authenticate_whenUserCancels_shouldThrowException() { assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenUserRefusedGeneral.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); makeAuthenticationRequest(); }); } @@ -738,8 +738,8 @@ void authenticate_whenUserCancels_shouldThrowException() { @Test void authenticate_whenTimeout_shouldThrowException() { assertThrows(SessionTimeoutException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenTimeout.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenTimeout.json"); makeAuthenticationRequest(); }); } @@ -747,8 +747,8 @@ void authenticate_whenTimeout_shouldThrowException() { @Test void authenticate_whenDocumentUnusable_shouldThrowException() { assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusWhenDocumentUnusable.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenDocumentUnusable.json"); makeAuthenticationRequest(); }); } @@ -756,7 +756,7 @@ void authenticate_whenDocumentUnusable_shouldThrowException() { @Test void authenticate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json"); + stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json"); makeAuthenticationRequest(); }); } @@ -764,7 +764,7 @@ void authenticate_whenRequestForbidden_shouldThrowException() { @Test void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 480); + stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", 480); makeAuthenticationRequest(); }); } @@ -772,15 +772,15 @@ void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() @Test void authenticate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", 580); + stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", 580); makeAuthenticationRequest(); }); } @Test void setPollingSleepTimeoutForAuthentication() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); long duration = measureAuthenticationDuration(); assertTrue(duration > 2000L, "Duration is " + duration); @@ -790,8 +790,8 @@ void setPollingSleepTimeoutForAuthentication() { @Test void getDeviceIpAddress_ipAddressNotPresent() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); SmartIdAuthenticationResponse authentication = createAuthentication(); assertThat(authentication.getDeviceIpAddress(), is(nullValue())); @@ -799,8 +799,8 @@ void getDeviceIpAddress_ipAddressNotPresent() { @Test void getDeviceIpAddress_ipAddressReturned() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); + stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); SmartIdAuthenticationResponse authentication = createAuthentication(); assertThat(authentication.getDeviceIpAddress(), is("62.65.42.45")); @@ -808,7 +808,7 @@ void getDeviceIpAddress_ipAddressReturned() { @Test void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); String headerName = "custom-header"; String headerValue = "Hi!"; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java index 4f2b6b73..29ef95f5 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java @@ -33,12 +33,12 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubBadRequestResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.v2.SmartIdRestServiceStubs.stubUnauthorizedResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubBadRequestResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubUnauthorizedResponse; import static java.util.Arrays.asList; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,8 +57,6 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.SmartIdRestConnector; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; @@ -95,14 +93,14 @@ void getNotExistingSessionStatus() { @Test void getRunningSessionStatus() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunning.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunning.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); } @Test void getRunningSessionStatus_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusRunningWithIgnoredProperties.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunningWithIgnoredProperties.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); assertNotNull(sessionStatus.getIgnoredProperties()); @@ -113,7 +111,7 @@ void getRunningSessionStatus_withIgnoredProperties() { @Test void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulCertificateRequest.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); assertSuccessfulResponse(sessionStatus); assertNotNull(sessionStatus.getCert()); MatcherAssert.assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); @@ -122,7 +120,7 @@ void getSessionStatus_forSuccessfulCertificateRequest() { @Test void getSessionStatus_forSuccessfulSigningRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); assertSuccessfulResponse(sessionStatus); assertNotNull(sessionStatus.getSignature()); MatcherAssert.assertThat(sessionStatus.getSignature().getValue(), startsWith("luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv")); @@ -131,7 +129,7 @@ void getSessionStatus_forSuccessfulSigningRequest() { @Test void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusForSuccessfulSigningRequest.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); assertSuccessfulResponse(sessionStatus); verify(getRequestedFor(urlMatching("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) @@ -141,61 +139,61 @@ void getSessionStatus_hasUserAgentHeader() { @Test void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); } @Test void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); } @Test void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); } @Test void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedGeneral.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); } @Test void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenTimeout.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenTimeout.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); } @Test void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); } @Test void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/sessionStatusWhenDocumentUnusable.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenDocumentUnusable.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); } @Test void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); assertSuccessfulResponse(sessionStatus); @@ -204,7 +202,7 @@ void getSessionStatus_withTimeoutParameter() { @Test void getCertificate_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); CertificateRequest request = createDummyCertificateRequest(); CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); assertNotNull(response); @@ -213,7 +211,7 @@ void getCertificate_usingDocumentNumber() { @Test void getCertificate_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PASKZ-987654321012"); @@ -225,7 +223,7 @@ void getCertificate_usingSemanticsIdentifier() { @Test void getCertificate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); CertificateRequest request = createDummyCertificateRequest(); request.setNonce("zstOt2umlc"); CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); @@ -235,7 +233,7 @@ void getCertificate_withNonce_usingDocumentNumber() { @Test void getCertificate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequestWithNonce.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, "CZ", "1234567890"); CertificateRequest request = createDummyCertificateRequest(); request.setNonce("zstOt2umlc"); @@ -247,7 +245,7 @@ void getCertificate_withNonce_usingSemanticsIdentifier() { @Test void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -256,7 +254,7 @@ void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { @Test void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "requests/certificateChoiceRequest.json"); + stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "v2/requests/certificateChoiceRequest.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("IDCCZ-1234567890"); @@ -268,7 +266,7 @@ void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { @Test void getCertificate_withWrongAuthenticationParams_shuldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -277,7 +275,7 @@ void getCertificate_withWrongAuthenticationParams_shuldThrowException() { @Test void getCertificate_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -286,7 +284,7 @@ void getCertificate_withWrongRequestParams_shouldThrowException() { @Test void getCertificate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json"); + stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -295,7 +293,7 @@ void getCertificate_whenRequestForbidden_shouldThrowException() { @Test void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 480); + stubErrorResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", 480); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -304,7 +302,7 @@ void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException( @Test void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", 580); + stubErrorResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", 580); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); }); @@ -312,7 +310,7 @@ void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { @Test void sign_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); SignatureSessionResponse response = connector.sign("PNOEE-123456", request); assertNotNull(response); @@ -321,7 +319,7 @@ void sign_usingDocumentNumber() { @Test void sign_hasUserAgentHeader() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionResponse response = connector.sign("PNOEE-123456", createDummySignatureSessionRequest()); assertNotNull(response); @@ -332,7 +330,7 @@ void sign_hasUserAgentHeader() { @Test void sign_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequestWithNonce.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequestWithNonce.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); request.setNonce("zstOt2umlc"); SignatureSessionResponse response = connector.sign("PNOEE-123456", request); @@ -342,7 +340,7 @@ void sign_withNonce_usingDocumentNumber() { @Test void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); Interaction confirmationMessageInteraction = Interaction.confirmationMessage("Do you want to transfer 200 Bison dollars from savings account to Oceanic Airlines?"); @@ -356,7 +354,7 @@ void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTe @Test void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_noFallback.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_noFallback.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); Interaction confi = Interaction.confirmationMessage("Do you want to transfer 999 Bison dollars from savings account to Oceanic Airlines?"); @@ -369,7 +367,7 @@ void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { @Test void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); Interaction verificationCodeChoice = Interaction.verificationCodeChoice("Transfer 444 BSD to Oceanic Airlines?"); @@ -383,7 +381,7 @@ void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDispla @Test void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); @@ -397,7 +395,7 @@ void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificat @Test void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); @@ -412,7 +410,7 @@ void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeCho @Test void sign_whenDocumentNumberNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + stubNotFoundResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -421,7 +419,7 @@ void sign_whenDocumentNumberNotFound_shouldThrowException() { @Test void sign_withWrongAuthenticationParams_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + stubUnauthorizedResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -430,7 +428,7 @@ void sign_withWrongAuthenticationParams_shouldThrowException() { @Test void sign_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + stubBadRequestResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -439,7 +437,7 @@ void sign_withWrongRequestParams_shouldThrowException() { @Test void sign_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json"); + stubForbiddenResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -448,7 +446,7 @@ void sign_whenRequestForbidden_shouldThrowException() { @Test void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 480); + stubErrorResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", 480); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -457,7 +455,7 @@ void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { @Test void sign_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", 580); + stubErrorResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", 580); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); }); @@ -465,7 +463,7 @@ void sign_whenSystemUnderMaintenance_shouldThrowException() { @Test void authenticate_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); assertNotNull(response); @@ -474,7 +472,7 @@ void authenticate_usingDocumentNumber() { @Test void authenticate_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, "KZ", "987654321012"); @@ -486,7 +484,7 @@ void authenticate_usingSemanticsIdentifier() { @Test void authenticate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setNonce("g9rp4kjca3"); AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); @@ -496,7 +494,7 @@ void authenticate_withNonce_usingDocumentNumber() { @Test void authenticate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "requests/authenticationSessionRequestWithNonce.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "48308230504"); @@ -510,7 +508,7 @@ void authenticate_withNonce_usingSemanticsIdentifier() { @Test void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOLT-48010010101"); @@ -524,7 +522,7 @@ void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { @Test void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); @@ -535,7 +533,7 @@ void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { @Test void authenticate_hasUserAgentHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); @@ -549,7 +547,7 @@ void authenticate_hasUserAgentHeader() { @Test void authenticate_whenDocumentNumberNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + stubNotFoundResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -558,7 +556,7 @@ void authenticate_whenDocumentNumberNotFound_shouldThrowException() { @Test void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "requests/authenticationSessionRequest.json"); + stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "v2/requests/authenticationSessionRequest.json"); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.LV, "230883-19894"); @@ -570,7 +568,7 @@ void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { @Test void authenticate_withWrongAuthenticationParams_shuldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -579,7 +577,7 @@ void authenticate_withWrongAuthenticationParams_shuldThrowException() { @Test void authenticate_withWrongRequestParams_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + stubBadRequestResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -588,7 +586,7 @@ void authenticate_withWrongRequestParams_shouldThrowException() { @Test void authenticate_whenRequestForbidden_shouldThrowException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json"); + stubForbiddenResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -597,7 +595,7 @@ void authenticate_whenRequestForbidden_shouldThrowException() { @Test void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 480); + stubErrorResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", 480); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -606,7 +604,7 @@ void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() @Test void authenticate_whenSystemUnderMaintenance_shouldThrowException() { assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", 580); + stubErrorResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", 580); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); }); @@ -620,7 +618,7 @@ void verifyCustomRequestHeaderPresent_whenAuthenticating() { Map headers = new HashMap<>(); headers.put(headerName, headerValue); connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/authentication/document/PNOEE-123456", "requests/authenticationSessionRequest.json", "responses/authenticationSessionResponse.json"); + stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); connector.authenticate("PNOEE-123456", request); @@ -636,7 +634,7 @@ void verifyCustomRequestHeaderPresent_whenSigning() { Map headers = new HashMap<>(); headers.put(headerName, headerValue); connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/signature/document/PNOEE-123456", "requests/signatureSessionRequest.json", "responses/signatureSessionResponse.json"); + stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); SignatureSessionRequest request = createDummySignatureSessionRequest(); connector.sign("PNOEE-123456", request); @@ -652,7 +650,7 @@ void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { Map headers = new HashMap<>(); headers.put(headerName, headerValue); connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); CertificateRequest request = createDummyCertificateRequest(); connector.getCertificate("PNOEE-123456", request); @@ -663,7 +661,7 @@ void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { @Test void getCertificate_hasUserAgentHeader() { connector = new SmartIdRestConnector("http://localhost:18089"); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "requests/certificateChoiceRequest.json", "responses/certificateChoiceResponse.json"); + stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); connector.getCertificate("PNOEE-123456", createDummyCertificateRequest()); verify(postRequestedFor(urlMatching("/certificatechoice/document/PNOEE-123456")) @@ -679,7 +677,7 @@ void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { Map headers = new HashMap<>(); headers.put(headerName, headerValue); connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/sessionStatusForSuccessfulCertificateRequest.json"); + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index ce099830..1d019bdb 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -45,12 +45,10 @@ import ee.sk.smartid.v2.DigestCalculator; import ee.sk.smartid.v2.HashType; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.SmartIdRestConnector; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.SmartIdDemoIntegrationTest; +import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java new file mode 100644 index 00000000..d975b1eb --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -0,0 +1,529 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +public class DynamicLinkAuthenticationSessionRequestBuilderTest { + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + public void initAuthenticationSession_ok() { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); + assertEquals("DEMO", request.getRelyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V1, request.getSignatureProtocol()); + assertNotNull(request.getSignatureProtocolParameters()); + assertNotNull(request.getSignatureProtocolParameters().getRandomChallenge()); + assertEquals("sha512WithRSAEncryption", request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertNotNull(request.getAllowedInteractionsOrder()); + assertTrue(request.getAllowedInteractionsOrder().stream().anyMatch(interaction -> interaction.getType().is("displayTextAndPIN"))); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + public void initAuthenticationSession_nonce_ok(String nonce) { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withNonce(nonce) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.getNonce()); + } + + @ParameterizedTest + @EnumSource + public void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withSignatureAlgorithm(signatureAlgorithm) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); + } + + @Test + public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNull(request.getRequestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withSharedMdClientIpAddress(ipRequested) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.getRequestProperties()); + assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + public void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withCapabilities(capabilities) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); + DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.getCapabilities()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName("DEMO") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName(relyingPartyName) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter randomChallenge must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRandomChallengeArgumentProvider.class) + public void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + public void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withSignatureAlgorithm(null) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidNonceProvider.class) + public void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withNonce(invalidNonce) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(interactions) + .initAuthenticationSession()); + assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(NotSupportedInteractionsProvider.class) + public void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(List.of(interaction)) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInteractionsProvider.class) + public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(List.of(interaction)) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var exception = assertThrows(SmartIdClientException.class, () -> { + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session ID is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { + var exception = assertThrows(SmartIdClientException.class, () -> { + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionResponse.setSessionToken(sessionToken); + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session token is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + public void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { + var exception = assertThrows(SmartIdClientException.class, () -> { + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); + dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); + when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session secret is missing from the response", exception.getMessage()); + } + + private void initAuthentication() { + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + } + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @Test + void initAuthenticationSession_withDocumentNumber() { + when(connector.initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DynamicLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + private DynamicLinkAuthenticationSessionResponse createDynamicLinkAuthenticationResponse() { + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); + dynamicLinkAuthenticationSessionResponse.setSessionSecret(generateBase64String("sessionSecret")); + return dynamicLinkAuthenticationSessionResponse; + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[0], Collections.emptySet()), + Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), + Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) + ); + } + } + + private static class InvalidRandomChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Parameter randomChallenge is not a valid Base64 encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(31).getBytes())), + "Size of parameter randomChallenge must be between 32 and 64 bytes"), + Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(65).getBytes())), + "Size of parameter randomChallenge must be between 32 and 64 bytes") + ); + } + } + + private static class InvalidNonceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") + ); + } + } + + private static class NotSupportedInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("VERIFICATION_CODE_CHOICE interaction used", Interaction.verificationCodeChoice("Log into internet banking system")), + "AllowedInteractionsOrder contains not supported interaction VERIFICATION_CODE_CHOICE"), + Arguments.of(Named.of("CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE interaction used", Interaction.confirmationMessageAndVerificationCodeChoice("Log into internet banking system")), + "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE") + ); + } + } + + private static class InvalidInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided text is null", Interaction.displayTextAndPIN(null)), + "displayText60 cannot be null for AllowedInteractionOrder of type DISPLAY_TEXT_AND_PIN"), + Arguments.of(Named.of("provided text is longer than allowed 60", Interaction.displayTextAndPIN("a".repeat(61))), + "displayText60 must not be longer than 60 characters"), + Arguments.of(Named.of("provided text is null", Interaction.confirmationMessage(null)), + "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE"), + Arguments.of(Named.of("provided text is longer than allowed 200", Interaction.confirmationMessage("a".repeat(201))), + "displayText200 must not be longer than 200 characters") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java b/src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java new file mode 100644 index 00000000..1512cb64 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java @@ -0,0 +1,68 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class RandomChallengeTest { + + @Test + void generate_defaultValueUsed() { + String challenge = RandomChallenge.generate(); + + assertNotNull(challenge); + byte[] decodeChallenge = Base64.decode(challenge); + assertEquals(64, decodeChallenge.length); + } + + @ParameterizedTest + @ValueSource(ints = {32, 43, 59, 64}) + void generate_providedValuesAreInAllowedRange(int allowedValue) { + String challenge = RandomChallenge.generate(allowedValue); + assertNotNull(challenge); + byte[] decodeChallenge = Base64.decode(challenge); + assertEquals(allowedValue, decodeChallenge.length); + } + + @Test + void generate_providedValueIsLessThanAllowed_throwException() { + assertThrows(IllegalArgumentException.class, () -> RandomChallenge.generate(31)); + } + + @Test + void generate_providedValueIsMoreThanAllowed_throwException() { + assertThrows(IllegalArgumentException.class, () -> RandomChallenge.generate(65)); + } + +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java new file mode 100644 index 00000000..d8283b63 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -0,0 +1,109 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +@WireMockTest(httpPort = 18089) +public class SmartIdClientTest { + + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("http://localhost:18089"); + smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); + } + + @Test + void createDynamicLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java new file mode 100644 index 00000000..72620902 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -0,0 +1,174 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.SignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +class SmartIdRestConnectorTest { + + @Nested + @WireMockTest(httpPort = 18081) + class AnonymousDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18081"); + } + + @Test + void initAnonymousDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + + assertNotNull(response); + } + + @Test + void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + }); + } + + @Test + void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + + assertNotNull(response); + } + + @Test + void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + + @Test + void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + + assertNotNull(response); + } + + @Test + void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + + @Test + void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + } + + private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + var dynamicLinkAuthenticationSessionRequest = new DynamicLinkAuthenticationSessionRequest(); + dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); + + var signatureProtocolParameters = new SignatureProtocolParameters(); + signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); + signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); + + Interaction interaction = Interaction.displayTextAndPIN("Log in?"); + dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); + + return dynamicLinkAuthenticationSessionRequest; + } +} diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java new file mode 100644 index 00000000..dc8a9bd0 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java @@ -0,0 +1,95 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.RandomChallenge; +import ee.sk.smartid.v3.SignatureAlgorithm; +import ee.sk.smartid.v3.SignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +@Disabled("Currently request to v3 path returns - No permission to issue the request") +@SmartIdDemoIntegrationTest +public class SmartIdRestIntegrationTest { + + private SmartIdConnector smartIdConnector; + + @BeforeEach + void setUp() { + smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); + } + + @Test + void authenticate_anonymous() { + DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + + DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); + } + + @Test + void authenticate_withDocumentNumber() { + DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + + DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-50609019996-MOCK-Q"); + } + + @Test + void authenticate_withSemanticsIdentifier() { + DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + + DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); + } + + private static DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + DynamicLinkAuthenticationSessionRequest request = new DynamicLinkAuthenticationSessionRequest(); + request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + request.setRelyingPartyName("DEMO"); + request.setCertificateLevel("QUALIFIED"); + + String randomChallenge = RandomChallenge.generate(); + var signatureParameters = new SignatureProtocolParameters(); + signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureParameters.setRandomChallenge(randomChallenge); + request.setSignatureProtocolParameters(signatureParameters); + return request; + } +} diff --git a/src/test/resources/requests/authenticationSessionRequest.json b/src/test/resources/v2/requests/authenticationSessionRequest.json similarity index 100% rename from src/test/resources/requests/authenticationSessionRequest.json rename to src/test/resources/v2/requests/authenticationSessionRequest.json diff --git a/src/test/resources/requests/authenticationSessionRequestWithNonce.json b/src/test/resources/v2/requests/authenticationSessionRequestWithNonce.json similarity index 100% rename from src/test/resources/requests/authenticationSessionRequestWithNonce.json rename to src/test/resources/v2/requests/authenticationSessionRequestWithNonce.json diff --git a/src/test/resources/requests/authenticationSessionRequestWithSingleAllowedInteraction.json b/src/test/resources/v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json similarity index 100% rename from src/test/resources/requests/authenticationSessionRequestWithSingleAllowedInteraction.json rename to src/test/resources/v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json diff --git a/src/test/resources/requests/certificateChoiceRequest.json b/src/test/resources/v2/requests/certificateChoiceRequest.json similarity index 100% rename from src/test/resources/requests/certificateChoiceRequest.json rename to src/test/resources/v2/requests/certificateChoiceRequest.json diff --git a/src/test/resources/requests/certificateChoiceRequestWithNonce.json b/src/test/resources/v2/requests/certificateChoiceRequestWithNonce.json similarity index 100% rename from src/test/resources/requests/certificateChoiceRequestWithNonce.json rename to src/test/resources/v2/requests/certificateChoiceRequestWithNonce.json diff --git a/src/test/resources/requests/signatureSessionRequest.json b/src/test/resources/v2/requests/signatureSessionRequest.json similarity index 100% rename from src/test/resources/requests/signatureSessionRequest.json rename to src/test/resources/v2/requests/signatureSessionRequest.json diff --git a/src/test/resources/requests/signatureSessionRequestWithNonce.json b/src/test/resources/v2/requests/signatureSessionRequestWithNonce.json similarity index 100% rename from src/test/resources/requests/signatureSessionRequestWithNonce.json rename to src/test/resources/v2/requests/signatureSessionRequestWithNonce.json diff --git a/src/test/resources/requests/signatureSessionRequestWithRequestProperties.json b/src/test/resources/v2/requests/signatureSessionRequestWithRequestProperties.json similarity index 100% rename from src/test/resources/requests/signatureSessionRequestWithRequestProperties.json rename to src/test/resources/v2/requests/signatureSessionRequestWithRequestProperties.json diff --git a/src/test/resources/requests/signatureSessionRequestWithSha512.json b/src/test/resources/v2/requests/signatureSessionRequestWithSha512.json similarity index 100% rename from src/test/resources/requests/signatureSessionRequestWithSha512.json rename to src/test/resources/v2/requests/signatureSessionRequestWithSha512.json diff --git a/src/test/resources/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json b/src/test/resources/v2/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json similarity index 100% rename from src/test/resources/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json rename to src/test/resources/v2/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json b/src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json similarity index 100% rename from src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json rename to src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json b/src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json similarity index 100% rename from src/test/resources/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json rename to src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json diff --git a/src/test/resources/requests/signingRequest_confirmationMessage_noFallback.json b/src/test/resources/v2/requests/signingRequest_confirmationMessage_noFallback.json similarity index 100% rename from src/test/resources/requests/signingRequest_confirmationMessage_noFallback.json rename to src/test/resources/v2/requests/signingRequest_confirmationMessage_noFallback.json diff --git a/src/test/resources/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json b/src/test/resources/v2/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json similarity index 100% rename from src/test/resources/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json rename to src/test/resources/v2/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json diff --git a/src/test/resources/responses/authenticationSessionResponse.json b/src/test/resources/v2/responses/authenticationSessionResponse.json similarity index 100% rename from src/test/resources/responses/authenticationSessionResponse.json rename to src/test/resources/v2/responses/authenticationSessionResponse.json diff --git a/src/test/resources/responses/certificateChoiceResponse.json b/src/test/resources/v2/responses/certificateChoiceResponse.json similarity index 100% rename from src/test/resources/responses/certificateChoiceResponse.json rename to src/test/resources/v2/responses/certificateChoiceResponse.json diff --git a/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequest.json b/src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json similarity index 100% rename from src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequest.json rename to src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json diff --git a/src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json b/src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json similarity index 100% rename from src/test/resources/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json rename to src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json diff --git a/src/test/resources/responses/sessionStatusForSuccessfulCertificateRequest.json b/src/test/resources/v2/responses/sessionStatusForSuccessfulCertificateRequest.json similarity index 100% rename from src/test/resources/responses/sessionStatusForSuccessfulCertificateRequest.json rename to src/test/resources/v2/responses/sessionStatusForSuccessfulCertificateRequest.json diff --git a/src/test/resources/responses/sessionStatusForSuccessfulSigningRequest.json b/src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequest.json similarity index 100% rename from src/test/resources/responses/sessionStatusForSuccessfulSigningRequest.json rename to src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequest.json diff --git a/src/test/resources/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json b/src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json similarity index 100% rename from src/test/resources/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json rename to src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json diff --git a/src/test/resources/responses/sessionStatusRunning.json b/src/test/resources/v2/responses/sessionStatusRunning.json similarity index 100% rename from src/test/resources/responses/sessionStatusRunning.json rename to src/test/resources/v2/responses/sessionStatusRunning.json diff --git a/src/test/resources/responses/sessionStatusRunningWithIgnoredProperties.json b/src/test/resources/v2/responses/sessionStatusRunningWithIgnoredProperties.json similarity index 100% rename from src/test/resources/responses/sessionStatusRunningWithIgnoredProperties.json rename to src/test/resources/v2/responses/sessionStatusRunningWithIgnoredProperties.json diff --git a/src/test/resources/responses/sessionStatusWhenDocumentUnusable.json b/src/test/resources/v2/responses/sessionStatusWhenDocumentUnusable.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenDocumentUnusable.json rename to src/test/resources/v2/responses/sessionStatusWhenDocumentUnusable.json diff --git a/src/test/resources/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json b/src/test/resources/v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json rename to src/test/resources/v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json diff --git a/src/test/resources/responses/sessionStatusWhenTimeout.json b/src/test/resources/v2/responses/sessionStatusWhenTimeout.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenTimeout.json rename to src/test/resources/v2/responses/sessionStatusWhenTimeout.json diff --git a/src/test/resources/responses/sessionStatusWhenUnknownErrorCode.json b/src/test/resources/v2/responses/sessionStatusWhenUnknownErrorCode.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUnknownErrorCode.json rename to src/test/resources/v2/responses/sessionStatusWhenUnknownErrorCode.json diff --git a/src/test/resources/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json b/src/test/resources/v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json rename to src/test/resources/v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedCertChoice.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedCertChoice.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedCertChoice.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedCertChoice.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessage.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessage.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedGeneral.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedGeneral.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedGeneral.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedGeneral.json diff --git a/src/test/resources/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json b/src/test/resources/v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json similarity index 100% rename from src/test/resources/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json rename to src/test/resources/v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json diff --git a/src/test/resources/responses/signatureSessionResponse.json b/src/test/resources/v2/responses/signatureSessionResponse.json similarity index 100% rename from src/test/resources/responses/signatureSessionResponse.json rename to src/test/resources/v2/responses/signatureSessionResponse.json diff --git a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json new file mode 100644 index 00000000..b5429a87 --- /dev/null +++ b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json @@ -0,0 +1,12 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V1", + "signatureProtocolParameters": { + "signatureAlgorithm": "sha512WithRSAEncryption", + "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" + }, + "allowedInteractionsOrder": [ + {"type": "displayTextAndPIN", "displayText60": "Log in?"} + ] +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json b/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json new file mode 100644 index 00000000..24832a88 --- /dev/null +++ b/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json @@ -0,0 +1,5 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "sessionToken": "sessionToken", + "sessionSecret": "sessionSecret" +} \ No newline at end of file From dda8ea27b0a3a78a9d9c536dddcf78985718a903 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Tue, 29 Oct 2024 07:40:40 +0200 Subject: [PATCH 05/57] SLIB-51 - implemented RP API v3 session status request handling (#88) * SLIB-51 - implemented RP API v3 session status request handling * SLIB-51 - added tests, code review changes * SLIB-51 - refactored testklasses, removed timeoutMs as parameter, code review changes * SLIB-51 - updated doc, README, code review changes * SLIB-51 - generated license headers * SLIB-51 - updated changelog * SLIB-51 - updated changelog --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 1 + README.md | 156 ++++++ .../smartid/{v2 => }/CertificateParser.java | 2 +- .../java/ee/sk/smartid/{v2 => }/HashType.java | 2 +- .../ee/sk/smartid/v2/AuthenticationHash.java | 2 + .../v2/AuthenticationRequestBuilder.java | 1 + .../smartid/v2/CertificateRequestBuilder.java | 1 + .../ee/sk/smartid/v2/DigestCalculator.java | 1 + .../java/ee/sk/smartid/v2/SignableData.java | 2 + .../java/ee/sk/smartid/v2/SignableHash.java | 70 +-- .../smartid/v2/SignatureRequestBuilder.java | 1 + .../v2/SmartIdAuthenticationResponse.java | 1 + .../sk/smartid/v2/SmartIdRequestBuilder.java | 1 + .../v2/VerificationCodeCalculator.java | 2 + .../ee/sk/smartid/v3/DigestCalculator.java | 46 ++ .../java/ee/sk/smartid/v3/SignableData.java | 74 +++ .../java/ee/sk/smartid/v3/SignableHash.java | 75 +++ .../v3/SmartIdAuthenticationResponse.java | 146 ++++++ .../java/ee/sk/smartid/v3/SmartIdClient.java | 24 +- .../smartid/v3/rest/SessionStatusPoller.java | 45 +- .../sk/smartid/v3/rest/SmartIdConnector.java | 11 + .../smartid/v3/rest/SmartIdRestConnector.java | 188 +++---- .../v3/rest/dao/SessionCertificate.java | 53 ++ .../sk/smartid/v3/rest/dao/SessionResult.java | 16 +- .../smartid/v3/rest/dao/SessionSignature.java | 72 +++ .../sk/smartid/v3/rest/dao/SessionStatus.java | 136 ++--- .../dao/SignatureAlgorithmParameters.java | 44 ++ .../v3/rest/dao/SignatureProtocol.java | 32 ++ .../service/SmartIdRequestBuilderService.java | 257 ++++++++++ .../{v2 => }/CertificateParserTest.java | 4 +- .../sk/smartid/{v2 => }/SignableDataTest.java | 3 +- .../sk/smartid/{v2 => }/SignableHashTest.java | 5 +- .../ee/sk/smartid/integration/ReadmeTest.java | 4 +- .../v2/AuthenticationRequestBuilderTest.java | 1 + .../AuthenticationResponseValidatorTest.java | 2 + .../v2/CertificateRequestBuilderTest.java | 1 + .../ee/sk/smartid/v2/CertificateUtil.java | 2 +- .../sk/smartid/v2/DigestCalculatorTest.java | 3 +- .../v2/SignatureRequestBuilderTest.java | 1 + .../v2/SmartIdAuthenticationResponseTest.java | 1 + .../ee/sk/smartid/v2/SmartIdClientTest.java | 1 + .../v2/VerificationCodeCalculatorTest.java | 4 +- .../v2/rest/SmartIdRestIntegrationTest.java | 2 +- .../v3/rest/SmartIdRestConnectorTest.java | 147 +++++- .../SmartIdRequestBuilderServiceTest.java | 473 ++++++++++++++++++ 45 files changed, 1889 insertions(+), 227 deletions(-) rename src/main/java/ee/sk/smartid/{v2 => }/CertificateParser.java (98%) rename src/main/java/ee/sk/smartid/{v2 => }/HashType.java (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/DigestCalculator.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignableData.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignableHash.java create mode 100644 src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java create mode 100644 src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java rename src/test/java/ee/sk/smartid/{v2 => }/CertificateParserTest.java (95%) rename src/test/java/ee/sk/smartid/{v2 => }/SignableDataTest.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/SignableHashTest.java (97%) create mode 100644 src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a35603b..44aae756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support for Smart-ID API v3.0 has been added under the ee.sk.smartid.v3 package. - Added handling for dynamic-link authentication session requests. View V3 section in README.md for more information. +- Added support for handling session status requests. View V3 section in README.md for more information. ### Changed - Existing code for Smart-ID API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. diff --git a/README.md b/README.md index 17ec46b1..6b236ece 100644 --- a/README.md +++ b/README.md @@ -708,6 +708,162 @@ String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse ``` + +## Session status request handling for v3.0 + +The Smart-ID v3.0 API includes new session status request paths for retrieving session results. + +## Session status endpoint +* Method: `GET` +* Path: `BASE/v3/session/:sessionId` +* Query parameter: `timeoutMs` (optional, long poll timeout value, default is halfway between max and min values) + +Example of the endpoint: +https://rp-api.smart-id.com/v3/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000 + +## Session status response + +The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: + +* `state`: RUNNING or COMPLETE +* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) +* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature sessions) +* `signature`: Contains the following fields based on the signatureProtocol used: + * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm + * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm +* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). +* `ignoredProperties`: Any unsupported or ignored properties from the request. +* `interactionFlowUsed`: The interaction flow used for the session. +* `deviceIpAddress`: IP address of the mobile device, if requested. + +### Successful response when still waiting for user’s response + +```json +{ + "state": "RUNNING" +} +``` + +### ACSP_V1 is returned in the session status OK response for authentication sessions in both dynamic-link and notification-based flows. + +```json + { + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-372...." + }, + "signatureProtocol": "ACSP_V1", + "signature": { + "serverRandom": "B+C9XVjIAZnCHH9vfBSv...", + "value": "B+A9CfjIBZnDHHav3B4F...", + "signatureAlgorithm": "sha512WithRSAEncryption", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "cert": { + "value": "B+C9XVjIAZnCHH9vfBSv...", + "certificateLevel": "QUALIFIED" + } +} +``` + +### RAW_DIGEST_SIGNATURE is returned in the session status OK response for signature sessions in both dynamic link and notification-based flows. + +```json +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-372...." + }, + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signature": { + "value": "B+A9CfjIBZnDHHav3B4F...", + "signatureAlgorithm": "sha512WithRSAEncryption", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "cert": { + "value": "B+C9XVjIAZnCHH9vfBSv...", + "certificateLevel": "QUALIFIED" + } +} +``` + +## Example of fetching session status in v3.0 + +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Client setup with TrustStore. Requests will not work without a valid certificate. + InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(is, "changeit".toCharArray()); + client.setTrustStore(trustStore); + +var poller = new SessionStatusPoller(client.getSmartIdConnector(), new SmartIdRequestBuilderService()); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); + +if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { +System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); +} +``` + +## Validating session status response + +It's important to validate the session status response to ensure that the returned signature or authentication result is valid. + +* Validate that endResult is OK if the session was successful. +* Check the certificate field to ensure it has the required certificate level and that it is signed by a trusted CA. +* For `ACSP_V1` signature validation, compare the digest of the signature protocol, server random, and random challenge. +* For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. + +### Example of validating the signature: + +```java +SmartIdRequestBuilderService requestBuilder = new SmartIdRequestBuilderService(); +requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); + +SmartIdAuthenticationResponse response = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); + +System.out.println("Authentication result: " + response.getEndResult()); +``` + +## Error handling for session status + +The session status response may return various error codes indicating the outcome of the session. Below are the possible endResult values for a completed session: + +* `OK`: Session completed successfully. +* `USER_REFUSED`: User refused the session. +* `TIMEOUT`: User did not respond in time. +* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. +* `WRONG_VC`: User selected the wrong verification code. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. +* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. +* `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). +* `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. +* `USER_REFUSED_CONFIRMATIONMESSAGE`: User cancelled on confirmationMessage screen. +* `USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE`: User cancelled on confirmationMessageAndVerificationCodeChoice screen. + +### The error codes can be validated using the SmartIdRequestBuilderService + +```java +try { + requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); +} catch (UserRefusedException e) { + System.out.println("User refused the session"); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out"); +} +``` Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. #### Initiating authentication session with document number diff --git a/src/main/java/ee/sk/smartid/v2/CertificateParser.java b/src/main/java/ee/sk/smartid/CertificateParser.java similarity index 98% rename from src/main/java/ee/sk/smartid/v2/CertificateParser.java rename to src/main/java/ee/sk/smartid/CertificateParser.java index e609fa3b..7902337f 100644 --- a/src/main/java/ee/sk/smartid/v2/CertificateParser.java +++ b/src/main/java/ee/sk/smartid/CertificateParser.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v2/HashType.java b/src/main/java/ee/sk/smartid/HashType.java similarity index 98% rename from src/main/java/ee/sk/smartid/v2/HashType.java rename to src/main/java/ee/sk/smartid/HashType.java index 87754b68..426c7f44 100644 --- a/src/main/java/ee/sk/smartid/v2/HashType.java +++ b/src/main/java/ee/sk/smartid/HashType.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java index 82a12930..5f0d1fd8 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java @@ -28,6 +28,8 @@ import java.security.SecureRandom; +import ee.sk.smartid.HashType; + /** * Class containing the hash and its hash type used for authentication */ diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java index 898b288c..b45809cb 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; diff --git a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java index a6122e05..0c04cceb 100644 --- a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.permanent.SmartIdClientException; diff --git a/src/main/java/ee/sk/smartid/v2/DigestCalculator.java b/src/main/java/ee/sk/smartid/v2/DigestCalculator.java index 1d65ab48..1529cb26 100644 --- a/src/main/java/ee/sk/smartid/v2/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/DigestCalculator.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import java.security.MessageDigest; diff --git a/src/main/java/ee/sk/smartid/v2/SignableData.java b/src/main/java/ee/sk/smartid/v2/SignableData.java index ab9555f3..f6bf7f96 100644 --- a/src/main/java/ee/sk/smartid/v2/SignableData.java +++ b/src/main/java/ee/sk/smartid/v2/SignableData.java @@ -29,6 +29,8 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.HashType; + /** * This class can be used to contain the data * to be signed when it is not yet in hashed format diff --git a/src/main/java/ee/sk/smartid/v2/SignableHash.java b/src/main/java/ee/sk/smartid/v2/SignableHash.java index f4356b40..583f6fdd 100644 --- a/src/main/java/ee/sk/smartid/v2/SignableHash.java +++ b/src/main/java/ee/sk/smartid/v2/SignableHash.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,6 +29,8 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.HashType; + /** * This class can be used to contain the hash * to be signed @@ -44,43 +46,43 @@ */ public class SignableHash implements Serializable { - private byte[] hash; - private HashType hashType; + private byte[] hash; + private HashType hashType; - public void setHash(byte[] hash) { - this.hash = hash.clone(); - } + public void setHash(byte[] hash) { + this.hash = hash.clone(); + } - public void setHashInBase64(String hashInBase64) { - hash = Base64.getDecoder().decode(hashInBase64); - } + public void setHashInBase64(String hashInBase64) { + hash = Base64.getDecoder().decode(hashInBase64); + } - public String getHashInBase64() { - return Base64.getEncoder().encodeToString(hash); - } + public String getHashInBase64() { + return Base64.getEncoder().encodeToString(hash); + } - public HashType getHashType() { - return hashType; - } + public HashType getHashType() { + return hashType; + } - public void setHashType(HashType hashType) { - this.hashType = hashType; - } + public void setHashType(HashType hashType) { + this.hashType = hashType; + } - public boolean areFieldsFilled() { - return hashType != null && hash != null && hash.length > 0; - } + public boolean areFieldsFilled() { + return hashType != null && hash != null && hash.length > 0; + } - /** - * Calculates the verification code from the hash - *

        - * Verification code should be displayed on the web page or some sort of web service - * so the person signing through the Smart-ID mobile app can verify if the verification code - * displayed on the phone matches with the one shown on the web page. - * - * @return the verification code - */ - public String calculateVerificationCode() { - return VerificationCodeCalculator.calculate(hash); - } + /** + * Calculates the verification code from the hash + *

        + * Verification code should be displayed on the web page or some sort of web service + * so the person signing through the Smart-ID mobile app can verify if the verification code + * displayed on the phone matches with the one shown on the web page. + * + * @return the verification code + */ + public String calculateVerificationCode() { + return VerificationCodeCalculator.calculate(hash); + } } diff --git a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java index 11e62d17..3a340d65 100644 --- a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java index 75967db5..a23d4c5c 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import java.io.Serializable; diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java index b6e458b4..330eb57e 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java @@ -26,6 +26,7 @@ * #L% */ +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; diff --git a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java index 798b619c..052af6ff 100644 --- a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java @@ -28,6 +28,8 @@ import java.nio.ByteBuffer; +import ee.sk.smartid.HashType; + public class VerificationCodeCalculator { /** diff --git a/src/main/java/ee/sk/smartid/v3/DigestCalculator.java b/src/main/java/ee/sk/smartid/v3/DigestCalculator.java new file mode 100644 index 00000000..625b1458 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DigestCalculator.java @@ -0,0 +1,46 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.MessageDigest; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public class DigestCalculator { + + public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { + try { + MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); + return digest.digest(dataToDigest); + } + catch (Exception e) { + throw new UnprocessableSmartIdResponseException("Problem with digest calculation. " + e); + } + } + +} diff --git a/src/main/java/ee/sk/smartid/v3/SignableData.java b/src/main/java/ee/sk/smartid/v3/SignableData.java new file mode 100644 index 00000000..aba0955f --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignableData.java @@ -0,0 +1,74 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.HashType; + +/** + * This class can be used to contain the data + * to be signed when it is not yet in hashed format + *

        + * {@link #setHashType(HashType)} can be used + * to set the wanted hash type. SHA-512 is default. + *

        + * {@link #calculateHash()} and + * {@link #calculateHashInBase64()} methods + * are used to calculate the hash for signing request. + *

        + * {@link SignableHash} can be used + * instead when the data to be signed is already + * in hashed format. + */ +public class SignableData implements Serializable { + + private final byte[] dataToSign; + private HashType hashType = HashType.SHA512; + + public SignableData(byte[] dataToSign) { + this.dataToSign = dataToSign.clone(); + } + + public String calculateHashInBase64() { + byte[] digest = calculateHash(); + return Base64.getEncoder().encodeToString(digest); + } + + public byte[] calculateHash() { + return DigestCalculator.calculateDigest(dataToSign, hashType); + } + + public void setHashType(HashType hashType) { + this.hashType = hashType; + } + + public HashType getHashType() { + return hashType; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SignableHash.java b/src/main/java/ee/sk/smartid/v3/SignableHash.java new file mode 100644 index 00000000..c90616e9 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignableHash.java @@ -0,0 +1,75 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.HashType; + +/** + * This class can be used to contain the hash + * to be signed + *

        + * {@link #setHash(byte[])} can be used + * to set the hash. + * {@link #setHashType(HashType)} can be used + * to set the hash type. + *

        + * {@link SignableData} can be used + * instead when the data to be signed is not already + * in hashed format. + */ +public class SignableHash implements Serializable { + + private byte[] hash; + private HashType hashType; + + public void setHash(byte[] hash) { + this.hash = hash.clone(); + } + + public void setHashInBase64(String hashInBase64) { + hash = Base64.getDecoder().decode(hashInBase64); + } + + public String getHashInBase64() { + return Base64.getEncoder().encodeToString(hash); + } + + public HashType getHashType() { + return hashType; + } + + public void setHashType(HashType hashType) { + this.hashType = hashType; + } + + public boolean areFieldsFilled() { + return hashType != null && hash != null && hash.length > 0; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java new file mode 100644 index 00000000..0b3c34a6 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java @@ -0,0 +1,146 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public class SmartIdAuthenticationResponse implements Serializable { + + private String endResult; + private String signedHashInBase64; + private HashType hashType; + private String signatureValueInBase64; + private String algorithmName; + private X509Certificate certificate; + private String requestedCertificateLevel; + private String certificateLevel; + private String documentNumber; + private String interactionFlowUsed; + private String deviceIpAddress; + + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + public String getEndResult() { + return endResult; + } + + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + public String getAlgorithmName() { + return algorithmName; + } + + public void setAlgorithmName(String algorithmName) { + this.algorithmName = algorithmName; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public String getSignedHashInBase64() { + return signedHashInBase64; + } + + public void setSignedHashInBase64(String signedHashInBase64) { + this.signedHashInBase64 = signedHashInBase64; + } + + public HashType getHashType() { + return hashType; + } + + public void setHashType(HashType hashType) { + this.hashType = hashType; + } + + public String getRequestedCertificateLevel() { + return requestedCertificateLevel; + } + + public void setRequestedCertificateLevel(String requestedCertificateLevel) { + this.requestedCertificateLevel = requestedCertificateLevel; + } + + public String getDocumentNumber() { + return documentNumber; + } + + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 9b4877cb..f5092366 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -44,9 +44,11 @@ import javax.net.ssl.TrustManagerFactory; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SessionStatusPoller; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.SmartIdRestConnector; import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.Configuration; public class SmartIdClient { @@ -176,10 +178,17 @@ public void setPollingSleepTimeout(TimeUnit unit, long timeout) { pollingSleepTimeout = timeout; } + private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { + connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + var sessionStatusPoller = new SessionStatusPoller(connector); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + return sessionStatusPoller; + } + public SmartIdConnector getSmartIdConnector() { if (null == connector) { - // Fallback to REST connector when not initialised - SmartIdRestConnector connector = configuredClient != null ? new SmartIdRestConnector(hostUrl, configuredClient) : new SmartIdRestConnector(hostUrl, networkConnectionConfig); + Client client = configuredClient != null ? configuredClient : createClient(); + SmartIdRestConnector connector = new SmartIdRestConnector(hostUrl, client); connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); if (trustSslContext == null && configuredClient == null) { @@ -236,4 +245,15 @@ public void setTrustedCertificates(String... sslCertificates) { public void setSmartIdConnector(SmartIdConnector smartIdConnector) { this.connector = smartIdConnector; } + + private Client createClient() { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (networkConnectionConfig != null) { + clientBuilder.withConfig(networkConnectionConfig); + } + if (trustSslContext != null) { + clientBuilder.sslContext(trustSslContext); + } + return clientBuilder.build(); + } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java index 77bea468..bf9c6351 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,10 +31,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + public class SessionStatusPoller { private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - private final SmartIdConnector connector; private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; private long pollingSleepTimeout = 1L; @@ -43,9 +45,38 @@ public SessionStatusPoller(SmartIdConnector connector) { this.connector = connector; } + public SessionStatus fetchFinalSessionStatus(String sessionId) { + logger.debug("Starting to poll session status for session {}", sessionId); + try { + return pollForFinalSessionStatus(sessionId); + } catch (InterruptedException ex) { + logger.error("Failed to poll session status", ex); + throw new SmartIdClientException("Failed to poll session status", ex); + } + } + + private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { + SessionStatus sessionStatus = null; + while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + sessionStatus = pollSessionStatus(sessionId); + if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + break; + } + logger.debug("Sleeping for {} {}", pollingSleepTimeout, pollingSleepTimeUnit); + pollingSleepTimeUnit.sleep(pollingSleepTimeout); + } + logger.debug("Got final session status response"); + return sessionStatus; + } + + private SessionStatus pollSessionStatus(String sessionId) { + logger.debug("Polling session status"); + return connector.getSessionStatus(sessionId); + } + public void setPollingSleepTime(TimeUnit unit, long timeout) { - logger.debug("Polling sleep time is " + timeout + " " + unit.toString()); - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; + logger.debug("Setting polling sleep time to {} {}", timeout, unit); + this.pollingSleepTimeUnit = unit; + this.pollingSleepTimeout = timeout; } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index 14c1c12d..ac03f5a4 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -31,12 +31,23 @@ import javax.net.ssl.SSLContext; +import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SessionStatus; public interface SmartIdConnector extends Serializable { + /** + * Get session status for the given session ID. + * + * @param sessionId The session ID + * @return The session status + * @throws SessionNotFoundException If the session is not found + */ + SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; + /** * Set the session status response socket open time * diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index ffe10e32..8318dd65 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -80,94 +80,86 @@ public class SmartIdRestConnector implements SmartIdConnector { private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; private transient SSLContext sslContext; + private long sessionStatusResponseSocketOpenTimeValue; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; public SmartIdRestConnector(String endpointUrl) { this.endpointUrl = endpointUrl; } - public SmartIdRestConnector(String endpointUrl, Configuration clientConfig) { - this(endpointUrl); - this.clientConfig = clientConfig; - } - public SmartIdRestConnector(String endpointUrl, Client configuredClient) { this(endpointUrl); this.configuredClient = configuredClient; } - private SessionStatus getSessionStatus(String sessionId, String path) throws SessionNotFoundException { + @Override + public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { + logger.debug("Getting session status for sessionId: {}", sessionId); SessionStatusRequest request = createSessionStatusRequest(sessionId); - UriBuilder uriBuilder = UriBuilder.fromUri(endpointUrl).path(path); + UriBuilder uriBuilder = UriBuilder + .fromUri(endpointUrl) + .path(SESSION_STATUS_URI); addResponseSocketOpenTimeUrlParameter(request, uriBuilder); - URI uri = uriBuilder.build(request.getSessionId()); + URI uri = uriBuilder.build(sessionId); + try { return prepareClient(uri).get(SessionStatus.class); - } catch (NotFoundException e) { - logger.warn("Session " + request + " not found: " + e.getMessage()); + } catch (NotFoundException ex) { + logger.warn("Session {} not found: {}", request, ex.getMessage()); throw new SessionNotFoundException(); } } - private T postRequest(URI uri, V request, Class responseType) { - try { - Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); - return prepareClient(uri).post(requestEntity, responseType); - } catch (NotAuthorizedException e) { - logger.warn("Request is unauthorized for URI " + uri, e); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); - } catch (BadRequestException e) { - logger.warn("Request is invalid for URI " + uri, e); - throw new SmartIdClientException("Server refused the request", e); - } catch (ClientErrorException e) { - if (e.getResponse().getStatus() == 471) { - logger.warn("No suitable account of requested type found, but user has some other accounts.", e); - throw new NoSuitableAccountOfRequestedTypeFoundException(); - } - if (e.getResponse().getStatus() == 472) { - logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", e); - throw new PersonShouldViewSmartIdPortalException(); - } - if (e.getResponse().getStatus() == 480) { - logger.warn("Client-side API is too old and not supported anymore"); - throw new SmartIdClientException("Client-side API is too old and not supported anymore"); - } - throw e; - } catch (ServerErrorException e) { - if (e.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", e); - throw new ServerMaintenanceException(); - } - throw e; - } + @Override + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + logger.debug("Starting dynamic link authentication session with semantics identifier"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); } - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - var request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; + @Override + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { + logger.debug("Starting dynamic link authentication session with document number"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); } - private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { - if (request.isResponseSocketOpenTimeSet()) { - long queryTimeoutInMilliseconds = sessionStatusResponseSocketOpenTimeUnit.toMillis(sessionStatusResponseSocketOpenTimeValue); - uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); - } + @Override + public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest) { + logger.debug("Starting anonymous dynamic link authentication session"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) + .build(); + return postAuthenticationRequest(uri, authenticationRequest); + } + + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; } protected Invocation.Builder prepareClient(URI uri) { Client client; if (this.configuredClient == null) { ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (clientConfig != null) { - clientBuilder.withConfig(clientConfig); + if (null != this.clientConfig) { + clientBuilder.withConfig(this.clientConfig); } - if (sslContext != null) { - clientBuilder.sslContext(sslContext); + if (null != this.sslContext) { + clientBuilder.sslContext(this.sslContext); } client = clientBuilder.build(); } else { @@ -199,44 +191,54 @@ protected String getJdkMajorVersion() { } } - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } - - @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } - - @Override - public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest) { - logger.debug("Starting anonymous dynamic link authentication session"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) - .build(); - return postAuthenticationRequest(uri, authenticationRequest); + private T postRequest(URI uri, V request, Class responseType) { + try { + Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); + return prepareClient(uri).post(requestEntity, responseType); + } catch (NotAuthorizedException e) { + logger.warn("Request is unauthorized for URI " + uri, e); + throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); + } catch (BadRequestException e) { + logger.warn("Request is invalid for URI " + uri, e); + throw new SmartIdClientException("Server refused the request", e); + } catch (ClientErrorException e) { + if (e.getResponse().getStatus() == 471) { + logger.warn("No suitable account of requested type found, but user has some other accounts.", e); + throw new NoSuitableAccountOfRequestedTypeFoundException(); + } + if (e.getResponse().getStatus() == 472) { + logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", e); + throw new PersonShouldViewSmartIdPortalException(); + } + if (e.getResponse().getStatus() == 480) { + logger.warn("Client-side API is too old and not supported anymore"); + throw new SmartIdClientException("Client-side API is too old and not supported anymore"); + } + throw e; + } catch (ServerErrorException e) { + if (e.getResponse().getStatus() == 580) { + logger.warn("Server is under maintenance, retry later", e); + throw new ServerMaintenanceException(); + } + throw e; + } } - @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - logger.debug("Starting dynamic link authentication session with semantics identifier"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postAuthenticationRequest(uri, authenticationRequest); + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + var request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; } - @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { - logger.debug("Starting dynamic link authentication session with document number"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postAuthenticationRequest(uri, authenticationRequest); + private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { + if (request.isResponseSocketOpenTimeSet()) { + TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); + long timeValue = request.getResponseSocketOpenTimeValue(); + long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); + uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); + } } private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI uri, DynamicLinkAuthenticationSessionRequest request) { diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java new file mode 100644 index 00000000..7dcdea7d --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java @@ -0,0 +1,53 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionCertificate implements Serializable { + private String value; + private String certificateLevel; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java index ba260090..7c30bf63 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java @@ -36,14 +36,6 @@ public class SessionResult implements Serializable { private String endResult; private String documentNumber; - public String getDocumentNumber() { - return documentNumber; - } - - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - public String getEndResult() { return endResult; } @@ -51,4 +43,12 @@ public String getEndResult() { public void setEndResult(String endResult) { this.endResult = endResult; } + + public String getDocumentNumber() { + return documentNumber; + } + + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java new file mode 100644 index 00000000..174e2fe2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java @@ -0,0 +1,72 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignature implements Serializable { + + private String value; + private String serverRandom; + private String signatureAlgorithm; + private SignatureAlgorithmParameters signatureAlgorithmParameters; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getServerRandom() { + return serverRandom; + } + + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java index 1f66abdd..6bb17728 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,71 +27,81 @@ */ import java.io.Serializable; -import java.util.Arrays; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import ee.sk.smartid.v2.rest.dao.SessionCertificate; -import ee.sk.smartid.v2.rest.dao.SessionSignature; @JsonIgnoreProperties(ignoreUnknown = true) public class SessionStatus implements Serializable { - private String state; - private SessionResult result; - - private String[] ignoredProperties = {}; - - private String interactionFlowUsed; - private String deviceIpAddress; - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public SessionResult getResult() { - return result; - } - - public void setResult(SessionResult result) { - this.result = result; - } - - public String[] getIgnoredProperties() { - return Arrays.copyOf(ignoredProperties, ignoredProperties.length); - } - - public void setIgnoredProperties(String[] ignoredProperties) { - this.ignoredProperties = Arrays.copyOf(ignoredProperties, ignoredProperties.length); - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * IP-address of the device running the App. - *

        - * Present only if withShareMdClientIpAddress() was specified with the request - * Also, the RelyingParty must be subscribed for the service. - * Also, the data must be available (e.g. not present in case state is TIMEOUT). - * @see Mobile Device IP sharing - * - * @return IP address of the device running Smart-ID app (or null if not returned) - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - + private String state; + private SessionResult result; + private String signatureProtocol; + private SessionSignature signature; + private SessionCertificate cert; + private String[] ignoredProperties; + private String interactionFlowUsed; + private String deviceIpAddress; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public SessionResult getResult() { + return result; + } + + public void setResult(SessionResult result) { + this.result = result; + } + + public String getSignatureProtocol() { + return signatureProtocol; + } + + public void setSignatureProtocol(String signatureProtocol) { + this.signatureProtocol = signatureProtocol; + } + + public SessionSignature getSignature() { + return signature; + } + + public void setSignature(SessionSignature signature) { + this.signature = signature; + } + + public SessionCertificate getCert() { + return cert; + } + + public void setCert(SessionCertificate cert) { + this.cert = cert; + } + + public String[] getIgnoredProperties() { + return ignoredProperties; + } + + public void setIgnoredProperties(String[] ignoredProperties) { + this.ignoredProperties = ignoredProperties; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java new file mode 100644 index 00000000..83208030 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java @@ -0,0 +1,44 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SignatureAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java new file mode 100644 index 00000000..b6760ee2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java @@ -0,0 +1,32 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public enum SignatureProtocol { + ACSP_V1, + RAW_DIGEST_SIGNATURE; +} diff --git a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java new file mode 100644 index 00000000..6d717b20 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java @@ -0,0 +1,257 @@ +package ee.sk.smartid.v3.service; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.HashType; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.SignableData; +import ee.sk.smartid.v3.SignableHash; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.v3.SmartIdAuthenticationResponse; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SignatureProtocol; + +public class SmartIdRequestBuilderService { + + private static final Logger logger = LoggerFactory.getLogger(SmartIdRequestBuilderService.class); + + protected SignableHash hashToSign; + protected SignableData dataToSign; + protected String relyingPartyUUID; + protected String relyingPartyName; + protected SemanticsIdentifier semanticsIdentifier; + + protected String documentNumber; + protected String certificateLevel; + protected String nonce; + protected Set capabilities; + protected List allowedInteractionsOrder; + + /** + * Create {@link SmartIdAuthenticationResponse} from {@link SessionStatus} + * + * @param sessionStatus session status response + * @return the authentication response + * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. + * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame + * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code + * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. + */ + public SmartIdAuthenticationResponse createSmartIdAuthenticationResponse(SessionStatus sessionStatus, String requestedCertificateLevel, + String expectedDigest, String randomChallenge) throws UserRefusedException, + UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + validateAuthenticationResponse(sessionStatus, requestedCertificateLevel, expectedDigest, randomChallenge); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate certificate = sessionStatus.getCert(); + + var authenticationResponse = new SmartIdAuthenticationResponse(); + authenticationResponse.setEndResult(sessionResult.getEndResult()); + authenticationResponse.setSignedHashInBase64(getHashInBase64()); + authenticationResponse.setHashType(getHashType()); + authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + authenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + authenticationResponse.setRequestedCertificateLevel(getCertificateLevel()); + authenticationResponse.setCertificateLevel(certificate.getCertificateLevel()); + authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + authenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return authenticationResponse; + } + + private void validateAuthenticationResponse(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest, String randomChallenge) { + if (sessionStatus == null) { + throw new UnprocessableSmartIdResponseException("Session status is null"); + } + validateSessionResult(sessionStatus, requestedCertificateLevel, expectedDigest, randomChallenge); + } + + public void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest, String randomChallenge) { + SessionResult sessionResult = sessionStatus.getResult(); + + if (sessionResult == null) { + logger.error("Result is missing in the session status response"); + throw new SmartIdClientException("Result is missing in the session status response"); + } + + String endResult = sessionResult.getEndResult(); + if ("OK".equalsIgnoreCase(endResult)) { + logger.info("Session completed successfully"); + + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + logger.error("Document number is missing in the session result"); + throw new SmartIdClientException("Document number is missing in the session result"); + } + + if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { + logger.error("InteractionFlowUsed is missing in the session status"); + throw new SmartIdClientException("InteractionFlowUsed is missing in the session status"); + } + + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + validateSignature(sessionStatus, expectedDigest, randomChallenge); + } else { + handleSessionEndResultErrors(endResult); + } + } + + protected HashType getHashType() { + if (hashToSign != null) { + return hashToSign.getHashType(); + } + return dataToSign.getHashType(); + } + + protected String getHashInBase64() { + if (hashToSign != null) { + return hashToSign.getHashInBase64(); + } + return dataToSign.calculateHashInBase64(); + } + + protected String getCertificateLevel() { + return certificateLevel; + } + + private void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { + if (sessionCertificate == null || sessionCertificate.getValue() == null) { + throw new SmartIdClientException("Missing certificate in session response"); + } + + try { + X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + cert.checkValidity(); + + if (!requestedCertificateLevel.equals(sessionCertificate.getCertificateLevel())) { + throw new CertificateLevelMismatchException(); + } + + } catch (Exception e) { + throw new SmartIdClientException("Certificate validation failed", e); + } + } + + private void validateSignature(SessionStatus sessionStatus, String expectedDigest, String randomChallenge) { + String signatureProtocol = sessionStatus.getSignatureProtocol(); + + if (SignatureProtocol.ACSP_V1.name().equals(signatureProtocol)) { + validateAcspV1Signature(sessionStatus, randomChallenge); + } else if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equals(signatureProtocol)) { + validateRawDigestSignature(sessionStatus, expectedDigest); + } else { + throw new SmartIdClientException("Unknown signature protocol: " + signatureProtocol); + } + } + + private void validateAcspV1Signature(SessionStatus sessionStatus, String randomChallenge) { + String signatureValue = sessionStatus.getSignature().getValue(); + String dataToHash = sessionStatus.getSignatureProtocol() + ";" + + Base64.getEncoder().encodeToString(sessionStatus.getSignature().getServerRandom().getBytes(StandardCharsets.UTF_8)) + ";" + + Base64.getEncoder().encodeToString(randomChallenge.getBytes(StandardCharsets.UTF_8)); + + try { + MessageDigest digest = MessageDigest.getInstance(sessionStatus.getSignature().getSignatureAlgorithmParameters().getHashAlgorithm()); + byte[] hashedData = digest.digest(dataToHash.getBytes(StandardCharsets.UTF_8)); + String expectedSignature = Base64.getEncoder().encodeToString(hashedData); + + if (!expectedSignature.equals(signatureValue)) { + throw new SmartIdClientException("ACSP_V1 signature validation failed. Expected: " + expectedSignature + + ", but got: " + signatureValue); + } + } catch (NoSuchAlgorithmException ex) { + throw new SmartIdClientException("Error while creating digest for ACSP_V1 signature validation", ex); + } + + logger.info("ACSP_V1 signature successfully validated."); + } + + private void validateRawDigestSignature(SessionStatus sessionStatus, String expectedDigest) { + String signatureValue = sessionStatus.getSignature().getValue(); + String signatureAlgorithm = sessionStatus.getSignature().getSignatureAlgorithm(); + + if (!expectedDigest.equals(signatureValue)) { + throw new SmartIdClientException("RAW_DIGEST_SIGNATURE validation failed. Expected: " + expectedDigest + + ", but got: " + signatureValue); + } + + Set allowedSignatureAlgorithms = Set.of("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); + if (!allowedSignatureAlgorithms.contains(signatureAlgorithm)) { + throw new SmartIdClientException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signatureAlgorithm); + } + + logger.info("RAW_DIGEST_SIGNATURE successfully validated."); + } + + private void handleSessionEndResultErrors(String endResult) { + switch (endResult.toUpperCase()) { + case "USER_REFUSED" -> throw new UserRefusedException(); + case "TIMEOUT" -> throw new SessionTimeoutException(); + case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); + case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); + case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); + case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); + case "USER_REFUSED_DISPLAYTEXTANDPIN" -> throw new UserRefusedDisplayTextAndPinException(); + case "USER_REFUSED_VC_CHOICE" -> throw new UserRefusedVerificationChoiceException(); + case "USER_REFUSED_CONFIRMATIONMESSAGE" -> throw new UserRefusedConfirmationMessageException(); + case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); + default -> throw new SmartIdClientException("Unexpected session result: " + endResult); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v2/CertificateParserTest.java b/src/test/java/ee/sk/smartid/CertificateParserTest.java similarity index 95% rename from src/test/java/ee/sk/smartid/v2/CertificateParserTest.java rename to src/test/java/ee/sk/smartid/CertificateParserTest.java index 990ae88e..9838b1ae 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/CertificateParserTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -31,7 +31,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v2.CertificateParser; +import ee.sk.smartid.CertificateParser; public class CertificateParserTest { diff --git a/src/test/java/ee/sk/smartid/v2/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/SignableDataTest.java rename to src/test/java/ee/sk/smartid/SignableDataTest.java index 813fb830..f86fdf4b 100644 --- a/src/test/java/ee/sk/smartid/v2/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -32,7 +32,6 @@ import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.HashType; import ee.sk.smartid.v2.SignableData; public class SignableDataTest { diff --git a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java b/src/test/java/ee/sk/smartid/SignableHashTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/v2/SignableHashTest.java rename to src/test/java/ee/sk/smartid/SignableHashTest.java index 364c8590..6bfab713 100644 --- a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/SignableHashTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -30,9 +30,8 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.DigestCalculator; -import ee.sk.smartid.v2.HashType; import ee.sk.smartid.v2.SignableHash; +import ee.sk.smartid.v2.DigestCalculator; public class SignableHashTest { diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java index 2dcfc2e9..74189922 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java @@ -60,8 +60,8 @@ import ee.sk.smartid.v2.AuthenticationHash; import ee.sk.smartid.v2.AuthenticationIdentity; import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.v2.CertificateParser; -import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.HashType; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; import ee.sk.smartid.FileUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java index 46c5d72d..c0d8f995 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java index 6a7b31af..236ba145 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java @@ -49,6 +49,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; diff --git a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java index 553ad317..25b0737b 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedException; diff --git a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java index 03fb9c56..8b891284 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java @@ -31,7 +31,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import ee.sk.smartid.v2.CertificateParser; +import ee.sk.smartid.CertificateParser; public final class CertificateUtil { diff --git a/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java index b83dcf38..e2e3caa4 100644 --- a/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java @@ -36,8 +36,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.v2.DigestCalculator; -import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.HashType; public class DigestCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java index 4e756b20..0406e314 100644 --- a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java @@ -44,6 +44,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java index 5f09148e..7a4339ce 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java @@ -35,6 +35,7 @@ import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; +import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java index 674ce6ab..b1954f58 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java @@ -67,6 +67,7 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; diff --git a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java index ab22c516..63454762 100644 --- a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java @@ -33,9 +33,7 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.DigestCalculator; -import ee.sk.smartid.v2.HashType; -import ee.sk.smartid.v2.VerificationCodeCalculator; +import ee.sk.smartid.HashType; public class VerificationCodeCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index 1d019bdb..e30de571 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -44,7 +44,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.v2.DigestCalculator; -import ee.sk.smartid.v2.HashType; +import ee.sk.smartid.HashType; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 72620902..9ca309de 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,10 +26,20 @@ * #L% */ +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.List; +import java.util.concurrent.TimeUnit; import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; @@ -38,6 +48,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; @@ -45,9 +56,141 @@ import ee.sk.smartid.v3.SignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SessionStatus; class SmartIdRestConnectorTest { + @Nested + @WireMockTest(httpPort = 18081) + class SessionStatusTests { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18081"); + } + + @Test + void getSessionStatus_running() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunning.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + } + + @Test + void getSessionStatus_running_withIgnoredProperties() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunningWithIgnoredProperties.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + assertNotNull(sessionStatus.getIgnoredProperties()); + assertEquals(2, sessionStatus.getIgnoredProperties().length); + assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); + assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); + } + + @Test + void getSessionStatus_forSuccessfulCertificateRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); + assertSuccessfulResponse(sessionStatus); + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_hasUserAgentHeader() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); + assertSuccessfulResponse(sessionStatus); + + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + void getSessionStatus_withTimeoutParameter() { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); + connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); + SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + assertSuccessfulResponse(sessionStatus); + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); + } + + @Test + void getSessionStatus_whenSessionNotFound() { + assertThrows(SessionNotFoundException.class, () -> { + stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); + connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + }); + } + + @Test + void getSessionStatus_userHasRefused() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessage() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); + } + + @Test + void getSessionStatus_userHasRefusedDisplayTextAndPin() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); + } + + @Test + void getSessionStatus_userHasRefusedVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); + } + + @Test + void getSessionStatus_timeout() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenTimeout.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); + } + + @Test + void getSessionStatus_userHasSelectedWrongVcCode() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); + } + + @Test + void getSessionStatus_whenDocumentUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenDocumentUnusable.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); + } + + private void assertSuccessfulResponse(SessionStatus sessionStatus) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertNotNull(sessionStatus.getResult()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + assertEquals("PNOEE-31111111111", sessionStatus.getResult().getDocumentNumber()); + } + + private void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals(endResult, sessionStatus.getResult().getEndResult()); + } + + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); + return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + } + } + @Nested @WireMockTest(httpPort = 18081) class AnonymousDynamicLinkAuthentication { diff --git a/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java b/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java new file mode 100644 index 00000000..8589b7ba --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java @@ -0,0 +1,473 @@ +package ee.sk.smartid.v3.service; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.v3.SignableData; +import ee.sk.smartid.v3.SmartIdAuthenticationResponse; +import ee.sk.smartid.v3.SmartIdClient; +import ee.sk.smartid.v3.rest.SessionStatusPoller; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SignatureAlgorithmParameters; + +class SmartIdRequestBuilderServiceTest { + + private SmartIdRequestBuilderService service; + + private static String DEMO_HOST_SSL_CERTIFICATE; + + @BeforeAll + static void loadCertificate() throws IOException { + try (InputStream is = SmartIdRequestBuilderServiceTest.class.getResourceAsStream("/sid_demo_sk_ee.pem"); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + + String certContent = reader.lines().collect(Collectors.joining("\n")); + + DEMO_HOST_SSL_CERTIFICATE = certContent + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replaceAll("\\s+", ""); + + } + } + + @BeforeEach + public void setUp() throws Exception { + service = new SmartIdRequestBuilderService(); + var client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + InputStream is = getClass().getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); + KeyStore trustStore = KeyStore.getInstance("JKS"); + trustStore.load(is, "changeit".toCharArray()); + client.setTrustStore(trustStore); + } + + @Test + void documentFetchingSessionStatus() { + SmartIdConnector connector = mock(SmartIdConnector.class); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + + var mockSessionStatus = new SessionStatus(); + mockSessionStatus.setState("COMPLETE"); + mockSessionStatus.setResult(sessionResult); + + when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); + + var poller = new SessionStatusPoller(connector); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + } + + @Test + void documentValidatingSessionStatus() throws Exception { + SmartIdConnector connector = mock(SmartIdConnector.class); + + String randomChallenge = "randomChallenge"; + SessionStatus mockSessionStatus = createMockSessionStatus(randomChallenge); + + when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); + + var poller = new SessionStatusPoller(connector); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); + + SmartIdRequestBuilderService requestBuilder = new SmartIdRequestBuilderService(); + + byte[] dataToSignBytes = "dataToBeSigned".getBytes(StandardCharsets.UTF_8); + var signableData = new SignableData(dataToSignBytes); + signableData.setHashType(HashType.SHA512); + + Field dataToSignField = SmartIdRequestBuilderService.class.getDeclaredField("dataToSign"); + dataToSignField.setAccessible(true); + dataToSignField.set(requestBuilder, signableData); + + requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", null, randomChallenge); + + SmartIdAuthenticationResponse response = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, randomChallenge); + + assertEquals("OK", response.getEndResult()); + assertEquals("QUALIFIED", response.getCertificateLevel()); + } + + @Test + void validateSessionResult_missingInteractionFlowUsed() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); + method.setAccessible(true); + + var sessionStatus = new SessionStatus(); + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + sessionStatus.setResult(sessionResult); + sessionStatus.setInteractionFlowUsed(null); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("InteractionFlowUsed is missing in the session status", ex.getCause().getMessage()); + } + + @Test + void validateSessionResult_nullSessionResult() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); + method.setAccessible(true); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(null); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Result is missing in the session status response", ex.getCause().getMessage()); + } + + @Test + void validateSessionResult_missingDocumentNumber() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); + method.setAccessible(true); + + var sessionStatus = new SessionStatus(); + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber(null); + + sessionStatus.setResult(sessionResult); + sessionStatus.setInteractionFlowUsed("someInteraction"); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Document number is missing in the session result", ex.getCause().getMessage()); + } + + @Nested + class CertificateValidation { + + @Test + void validateCertificate_missingCertificate() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); + method.setAccessible(true); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, null, "QUALIFIED")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Missing certificate in session response", ex.getCause().getMessage()); + } + + @Test + void validateCertificate_missingCertificateValue() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); + method.setAccessible(true); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(null); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Missing certificate in session response", ex.getCause().getMessage()); + } + + @Test + void validateCertificate_certificateLevelMismatch() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); + method.setAccessible(true); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); + sessionCertificate.setCertificateLevel("ADVANCED"); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertTrue(ex.getCause().getCause() instanceof CertificateLevelMismatchException); + } + + @Test + void validateCertificate_certificateParsingFails() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); + method.setAccessible(true); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("InvalidCertificateData"); + sessionCertificate.setCertificateLevel("QUALIFIED"); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Certificate validation failed", ex.getCause().getMessage()); + } + } + + @Nested + class SignatureValidation { + + @Test + void validateRawDigestSignature_successful() throws Exception { + SessionStatus mockSessionStatus = mock(SessionStatus.class); + SessionSignature mockSessionSignature = mock(SessionSignature.class); + + when(mockSessionSignature.getValue()).thenReturn("expectedDigest"); + when(mockSessionSignature.getSignatureAlgorithm()).thenReturn("sha512WithRSAEncryption"); + when(mockSessionStatus.getSignature()).thenReturn(mockSessionSignature); + + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(service, mockSessionStatus, "expectedDigest")); + } + + @Test + void validateSignature_withRawDigestSignatureProtocol() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSignature", SessionStatus.class, String.class, String.class); + method.setAccessible(true); + + var sessionStatus = new SessionStatus(); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("expectedDigest"); + sessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); + sessionStatus.setSignature(sessionSignature); + + assertDoesNotThrow(() -> method.invoke(service, sessionStatus, "expectedDigest", "randomChallenge")); + } + + @Test + void validateSignature_unknownProtocol() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSignature", SessionStatus.class, String.class, String.class); + method.setAccessible(true); + + var sessionStatus = new SessionStatus(); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest", "randomChallenge")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getCause().getMessage()); + } + + @Test + void validateAcspV1Signature_signatureMismatch() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateAcspV1Signature", SessionStatus.class, String.class); + method.setAccessible(true); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("InvalidSignatureValue"); + sessionSignature.setServerRandom("serverRandomValue"); + + var sigAlgParams = new SignatureAlgorithmParameters(); + sigAlgParams.setHashAlgorithm("SHA-256"); + sessionSignature.setSignatureAlgorithmParameters(sigAlgParams); + + var sessionStatus = new SessionStatus(); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol("ACSP_V1"); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "randomChallenge")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertTrue(ex.getCause().getMessage().contains("ACSP_V1 signature validation failed")); + } + + @Test + void validateRawDigestSignature_signatureMismatch() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); + method.setAccessible(true); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("actualDigest"); + sessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setSignature(sessionSignature); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertTrue(ex.getCause().getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); + } + + @Test + void validateRawDigestSignature_unexpectedAlgorithm() throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); + method.setAccessible(true); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("expectedDigest"); + sessionSignature.setSignatureAlgorithm("unexpectedAlgorithm"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setSignature(sessionSignature); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest")); + + assertTrue(ex.getCause() instanceof SmartIdClientException); + assertTrue(ex.getCause().getMessage().contains("Unexpected signature algorithm")); + } + } + + @Test + void documentFetchingSessionStatus_mocked() { + SmartIdConnector connector = mock(SmartIdConnector.class); + var mockSessionStatus = new SessionStatus(); + mockSessionStatus.setState("COMPLETE"); + + when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); + + var poller = new SessionStatusPoller(connector); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); + + assertEquals("COMPLETE", sessionStatus.getState()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void handleSessionEndResultErrors(String endResult, Class expectedException) throws Exception { + Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("handleSessionEndResultErrors", String.class); + method.setAccessible(true); + + var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, endResult)); + + assertEquals(expectedException, ex.getCause().getClass()); + } + + private static SessionStatus createMockSessionStatus(String randomChallenge) throws NoSuchAlgorithmException { + var mockSessionResult = new SessionResult(); + mockSessionResult.setEndResult("OK"); + mockSessionResult.setDocumentNumber("PNOEE-12345678901"); + + var mockCertificate = new SessionCertificate(); + mockCertificate.setCertificateLevel("QUALIFIED"); + mockCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); + + var mockSessionSignature = new SessionSignature(); + String serverRandom = "serverRandomValue"; + String signatureProtocol = "ACSP_V1"; + + String dataToHash = signatureProtocol + ";" + + Base64.getEncoder().encodeToString(serverRandom.getBytes(StandardCharsets.UTF_8)) + ";" + + Base64.getEncoder().encodeToString(randomChallenge.getBytes(StandardCharsets.UTF_8)); + + var sigAlgParams = new SignatureAlgorithmParameters(); + sigAlgParams.setHashAlgorithm("SHA-512"); + mockSessionSignature.setSignatureAlgorithmParameters(sigAlgParams); + + MessageDigest digest = MessageDigest.getInstance(sigAlgParams.getHashAlgorithm()); + byte[] hashedData = digest.digest(dataToHash.getBytes(StandardCharsets.UTF_8)); + String expectedSignature = Base64.getEncoder().encodeToString(hashedData); + + mockSessionSignature.setValue(expectedSignature); + mockSessionSignature.setServerRandom(serverRandom); + mockSessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); + + var mockSessionStatus = new SessionStatus(); + mockSessionStatus.setState("COMPLETE"); + mockSessionStatus.setResult(mockSessionResult); + mockSessionStatus.setCert(mockCertificate); + mockSessionStatus.setSignature(mockSessionSignature); + mockSessionStatus.setSignatureProtocol(signatureProtocol); + mockSessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + + return mockSessionStatus; + } + + static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), + Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) + ); + } + } +} \ No newline at end of file From 77cf4c9c3209a9b3bbf701d78ad022e72864d79e Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:00:19 +0200 Subject: [PATCH 06/57] Add dynamic link based certificate choice request handling (#90) * SLIB-51 - implemented RP API v3 session status request handling * SLIB-51 - added tests, code review changes * SLIB-51 - refactored testklasses, removed timeoutMs as parameter, code review changes * SLIB-54 - implemented dynamic link based certificate choice request handling * SLIB-54 - refactored CertificateLevel handling * SLIB-54 - refactoring, renaming, code review changes * SLIB-54 - generated license headers * SLIB-54 updated changelog * SLIB-54 - refactored SessionStore logic * SLIB-54 - refactoring Request properties/cert validation handling * SLIB-54 - renaming classes * SLIB-54 - improved CertificateChoiceStatusStore doc description * SLIB-54 - refactored testclass, removed CertificateChoiceStatusStore logic * SLIB-54 - moved testclass * SLIB-54 - updated/fixed README * SLIB-54 - updated documentation in README * SLIB-54 - updated code example in README --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 1 + README.md | 238 +++++++++--- .../sk/smartid/{v2 => }/DigestCalculator.java | 26 +- .../ee/sk/smartid/v2/AuthenticationHash.java | 1 + .../java/ee/sk/smartid/v2/SignableData.java | 1 + .../v2/VerificationCodeCalculator.java | 1 + ...tCalculator.java => CertificateLevel.java} | 21 +- .../java/ee/sk/smartid/v3/SignableData.java | 1 + .../java/ee/sk/smartid/v3/SmartIdClient.java | 80 ++-- .../sk/smartid/v3/rest/SmartIdConnector.java | 10 + .../smartid/v3/rest/SmartIdRestConnector.java | 66 +++- .../v3/rest/dao/CertificateRequest.java | 98 +++++ ...cLinkCertificateChoiceSessionResponse.java | 60 +++ ...ertificateChoiceSessionRequestBuilder.java | 182 +++++++++ .../service/SmartIdRequestBuilderService.java | 26 +- .../{v2 => }/DigestCalculatorTest.java | 3 +- .../smartid/SmartIdDemoIntegrationTest.java | 4 +- .../sk/smartid/SmartIdRestServiceStubs.java | 18 + .../sk/smartid/{ => v2}/SignableHashTest.java | 5 +- .../v2/VerificationCodeCalculatorTest.java | 1 + .../v2/rest/SmartIdRestIntegrationTest.java | 2 +- src/test/java/ee/sk/smartid/v3/FileUtil.java | 55 +++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 28 +- .../v3/rest/SmartIdRestConnectorTest.java | 133 ++++++- ...ficateChoiceSessionRequestBuilderTest.java | 216 +++++++++++ .../SmartIdRequestBuilderServiceTest.java | 365 +++++++----------- .../dynamicLinkCertificateChoiceResponse.json | 5 + ...namic-link-certificate-choice-request.json | 9 + ...amic-link-certificate-choice-response.json | 5 + .../v3/responses/session-status-not-ok.json | 6 + .../v3/responses/session-status-ok.json | 6 + 31 files changed, 1296 insertions(+), 377 deletions(-) rename src/main/java/ee/sk/smartid/{v2 => }/DigestCalculator.java (76%) rename src/main/java/ee/sk/smartid/v3/{DigestCalculator.java => CertificateLevel.java} (67%) create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java create mode 100644 src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java rename src/test/java/ee/sk/smartid/{v2 => }/DigestCalculatorTest.java (98%) rename src/test/java/ee/sk/smartid/{ => v2}/SignableHashTest.java (95%) create mode 100644 src/test/java/ee/sk/smartid/v3/FileUtil.java create mode 100644 src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java create mode 100644 src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json create mode 100644 src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json create mode 100644 src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json create mode 100644 src/test/resources/v3/responses/session-status-not-ok.json create mode 100644 src/test/resources/v3/responses/session-status-ok.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 44aae756..eccec6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for Smart-ID API v3.0 has been added under the ee.sk.smartid.v3 package. - Added handling for dynamic-link authentication session requests. View V3 section in README.md for more information. - Added support for handling session status requests. View V3 section in README.md for more information. +- Added support for handling dynamic link certificate choice requests. View V3 section in README.md for more information. ### Changed - Existing code for Smart-ID API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. diff --git a/README.md b/README.md index 6b236ece..813341f0 100644 --- a/README.md +++ b/README.md @@ -668,6 +668,76 @@ var client = new SmartIdClient(); ### Examples of performing authentication +#### Initiating authentication session with document number + +If you already know the documentNumber you can use this for (re-)authentication. + +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication + +// For security reasons a new hash value must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + // Smart-ID app will display verification code to the user and user must insert PIN1 + .withAllowedInteractionsOrder( + Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + +### Initiating anonymous authentication session + +Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. +RP can learn the user's identity only after the user has authenticated themselves. + +```java +// For security reasons a new hash value must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + // before the user can enter PIN. If user selects wrong verification code then the operation will fail. + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + #### Initiating authentication session with semantics identifier More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) @@ -708,6 +778,7 @@ String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse ``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. ## Session status request handling for v3.0 @@ -716,7 +787,7 @@ The Smart-ID v3.0 API includes new session status request paths for retrieving s ## Session status endpoint * Method: `GET` * Path: `BASE/v3/session/:sessionId` -* Query parameter: `timeoutMs` (optional, long poll timeout value, default is halfway between max and min values) +* Query parameter: `timeoutMs` (optional, long poll timeout value, default is halfway between max(120000ms) and min(1000ms) values) Example of the endpoint: https://rp-api.smart-id.com/v3/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000 @@ -813,7 +884,7 @@ var poller = new SessionStatusPoller(client.getSmartIdConnector(), new SmartIdRe SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { -System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); + System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); } ``` @@ -864,78 +935,135 @@ try { System.out.println("Session timed out"); } ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. -#### Initiating authentication session with document number +# Initiating a Dynamic Link Certificate Choice Session in API v3.0 -If you already know the documentNumber you can use this for (re-)authentication. +The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a certificate choice session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the authentication process. -```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication +## Dynamic Link Certificate Choice Endpoint -// For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response +To initiate a dynamic link certificate choice session, send a POST request to the following endpoint: -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); +* Method: `POST` +* Path: `BASE/v3/certificatechoice/dynamic-link/anonymous` -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +Example of the endpoint: +https://rp-api.smart-id.com/v3/certificatechoice/dynamic-link/anonymous -String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -// Store sessionSecret only on backend side. Do not expose it to the client side. +## Request Parameters -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +The request parameters for the dynamic link certificate choice session are: + +* `relyingPartyUUID`: UUID of the Relying Party. +* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. + +## Example: Initiating a Dynamic Link Certificate Choice Request +Here's an example of how to initiate a dynamic link certificate choice request using the Smart-ID Java client. + +```java +SmartIdClient client=new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel("QUALIFIED") + .withNonce("1234567890") + .withShareMdClientIpAddress(true) + .initiateCertificateChoice(); + +// Note: After a certificate choice request, a notification-based signature choice must follow. ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. -### Initiating anonymous authentication session +## Response on Successful Session Creation +The response from a successful dynamic link certificate choice session creation contains the following parameters: -Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. -RP can learn the user's identity only after the user has authenticated themselves. +* `sessionID`: A string that can be used to request the operation result. +* `sessionToken`: Unique random value that will be used to connect this certificate choice attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. + +### Example of a Successful Response +```json +{ + "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546014", + "sessionToken": "hyBdQYUeQtvXEPqWC7K8a97L", + "sessionSecret": "dztL7Ur49D/YYgUzYl4sMg==" +} +``` + +## Fetching Session Status +After initiating the dynamic link certificate choice session and storing the session information, you can fetch the session status to check if the user has completed the authentication process. ```java -// For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response +// Fetch the final session status +SessionStatusPoller poller = client.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + +// Validate the session status +var requestBuilder = new SmartIdRequestBuilderService(); +requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", null, null); + +// Create authentication response +SmartIdAuthenticationResponse authenticationResponse = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, null); + +// Extract user information +AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(authenticationResponse.getCertificate()); +String givenName = identity.getGivenName(); +String surname = identity.getSurname(); +String identityCode = identity.getIdentityCode(); +String country = identity.getCountry(); +``` -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client - .createAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - // before the user can enter PIN. If user selects wrong verification code then the operation will fail. - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .initAuthenticationSession(); +## Validating Parameters +Ensure that you validate the parameters before initiating the request. For example, the `nonce` must be between 1 and 30 characters. -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +## Error Handling +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `SmartIdClientException`, and others. -String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -// Store sessionSecret only on backend side. Do not expose it to the client side. +```java +try { + CertificateChoiceResponse response = builder.initiateCertificateChoice(); + // Proceed with session status fetching and validation +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (SmartIdClientException e) { + System.out.println("Client exception occurred: " + e.getMessage()); +} +``` -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +## Additional Information + +### `Request Properties`: If you need the IP address of the user's device, set only shareMdClientIpAddress to true. There is no need to create a full RequestProperties object for this. +```java +client.createDynamicLinkCertificateRequest().withShareMdClientIpAddress(true); ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +### `Capabilities`: The capabilities parameter is an optional field used only when an agreement is established with the Smart-ID provider. If this parameter is omitted, the requested capabilities are automatically derived from the `certificateLevel`. Supported certificate levels include: +* `ADVANCED`: A certificate for advanced electronic signatures. +* `QUALIFIED`: A qualified certificate under the eIDAS regulation. +* `QSCD`: A qualified certificate that is also QSCD-capable, marking a higher level of security for qualified signatures. + +### Example of Initiating a dynamic link certificate choice request with `QUALIFIED` certificate level and IP sharing enabled. +```java +SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withShareMdClientIpAddress(true) + .initiateCertificateChoice(); +``` ### Generating QR-code or dynamic link Todo: will be implemented in task SLIB-55 \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v2/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java similarity index 76% rename from src/main/java/ee/sk/smartid/v2/DigestCalculator.java rename to src/main/java/ee/sk/smartid/DigestCalculator.java index 1529cb26..f02f9978 100644 --- a/src/main/java/ee/sk/smartid/v2/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/DigestCalculator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,21 +26,19 @@ * #L% */ -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - import java.security.MessageDigest; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + public class DigestCalculator { - public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { - try { - MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); - return digest.digest(dataToDigest); - } - catch (Exception e) { - throw new UnprocessableSmartIdResponseException("Problem with digest calculation. " + e); + public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { + try { + MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); + return digest.digest(dataToDigest); + } catch (Exception e) { + throw new UnprocessableSmartIdResponseException("Problem with digest calculation. " + e); + } } - } } diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java index 5f0d1fd8..de2fa49a 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java @@ -28,6 +28,7 @@ import java.security.SecureRandom; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; /** diff --git a/src/main/java/ee/sk/smartid/v2/SignableData.java b/src/main/java/ee/sk/smartid/v2/SignableData.java index f6bf7f96..f2d6ba02 100644 --- a/src/main/java/ee/sk/smartid/v2/SignableData.java +++ b/src/main/java/ee/sk/smartid/v2/SignableData.java @@ -29,6 +29,7 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; /** diff --git a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java index 052af6ff..ea637b6b 100644 --- a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java @@ -28,6 +28,7 @@ import java.nio.ByteBuffer; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; public class VerificationCodeCalculator { diff --git a/src/main/java/ee/sk/smartid/v3/DigestCalculator.java b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java similarity index 67% rename from src/main/java/ee/sk/smartid/v3/DigestCalculator.java rename to src/main/java/ee/sk/smartid/v3/CertificateLevel.java index 625b1458..5b35a662 100644 --- a/src/main/java/ee/sk/smartid/v3/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,21 +26,6 @@ * #L% */ -import java.security.MessageDigest; - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -public class DigestCalculator { - - public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { - try { - MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); - return digest.digest(dataToDigest); - } - catch (Exception e) { - throw new UnprocessableSmartIdResponseException("Problem with digest calculation. " + e); - } - } - +public enum CertificateLevel { + ADVANCED, QUALIFIED, QSCD } diff --git a/src/main/java/ee/sk/smartid/v3/SignableData.java b/src/main/java/ee/sk/smartid/v3/SignableData.java index aba0955f..aa860507 100644 --- a/src/main/java/ee/sk/smartid/v3/SignableData.java +++ b/src/main/java/ee/sk/smartid/v3/SignableData.java @@ -29,6 +29,7 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; /** diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index f5092366..1baa337a 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -47,6 +47,7 @@ import ee.sk.smartid.v3.rest.SessionStatusPoller; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.SmartIdRestConnector; +import ee.sk.smartid.v3.service.DynamicLinkCertificateChoiceSessionRequestBuilder; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.Configuration; @@ -65,6 +66,10 @@ public class SmartIdClient { private SmartIdConnector connector; private SSLContext trustSslContext; + public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertificateRequest() { + return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()); + } + public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentication() { return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()); } @@ -178,13 +183,6 @@ public void setPollingSleepTimeout(TimeUnit unit, long timeout) { pollingSleepTimeout = timeout; } - private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - var sessionStatusPoller = new SessionStatusPoller(connector); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); - return sessionStatusPoller; - } - public SmartIdConnector getSmartIdConnector() { if (null == connector) { Client client = configuredClient != null ? configuredClient : createClient(); @@ -201,27 +199,26 @@ public SmartIdConnector getSmartIdConnector() { return connector; } - public static SSLContext createSslContext(List sslCertificates) - throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - CertificateFactory factory = CertificateFactory.getInstance("X509"); - int i = 0; - for (String sslCertificate : sslCertificates) { - Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); - keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); - } - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(keyStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext; - } - + /** + * Sets the SSL context for the client + *

        + * Useful for configuring custom SSL context + * for the client. + * + * @param trustSslContext SSL context for the client + */ public void setTrustSslContext(SSLContext trustSslContext) { this.trustSslContext = trustSslContext; } + /** + * Sets the trust store for the client + *

        + * Useful for configuring custom trust store + * for the client. + * + * @param trustStore trust store for the client + */ public void setTrustStore(KeyStore trustStore) { try { SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); @@ -246,6 +243,13 @@ public void setSmartIdConnector(SmartIdConnector smartIdConnector) { this.connector = smartIdConnector; } + private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { + connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + var sessionStatusPoller = new SessionStatusPoller(connector); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + return sessionStatusPoller; + } + private Client createClient() { ClientBuilder clientBuilder = ClientBuilder.newBuilder(); if (networkConnectionConfig != null) { @@ -256,4 +260,32 @@ private Client createClient() { } return clientBuilder.build(); } + + /** + * Creates an SSL context with the given certificates + * + * @param sslCertificates list of certificates in PEM format + * @return SSL context + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws IOException + * @throws CertificateException + * @throws KeyManagementException + */ + public static SSLContext createSslContext(List sslCertificates) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + int i = 0; + for (String sslCertificate : sslCertificates) { + Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); + keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(keyStore); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext; + } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index ac03f5a4..aaa9561a 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,6 +32,8 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v3.rest.dao.CertificateRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -56,6 +58,14 @@ public interface SmartIdConnector extends Serializable { */ void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); + /** + * Initiates a dynamic link based certificate choice request. + * + * @param request CertificateRequest containing necessary parameters + * @return CertificateChoiceResponse containing sessionID, sessionToken, and sessionSecret + */ + DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateRequest request); + /** * Set the SSL context to use for secure communication * diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index 8318dd65..dd4829b2 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -48,6 +48,8 @@ import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.CertificateRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.SessionStatus; import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; import jakarta.ws.rs.BadRequestException; @@ -72,6 +74,7 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + private static final String CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH = "/certificatechoice/dynamic-link/anonymous"; private static final String ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH = "authentication/dynamic-link/anonymous"; private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; @@ -141,8 +144,14 @@ public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthenti } @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; + public DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateRequest request) { + logger.debug("Initiating dynamic link based certificate choice request"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH) + .build(); + + return postCertificateRequest(uri, request); } @Override @@ -151,15 +160,20 @@ public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusRespons this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; } + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + protected Invocation.Builder prepareClient(URI uri) { Client client; if (this.configuredClient == null) { ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (null != this.clientConfig) { - clientBuilder.withConfig(this.clientConfig); + if (clientConfig != null) { + clientBuilder.withConfig(clientConfig); } - if (null != this.sslContext) { - clientBuilder.sslContext(this.sslContext); + if (sslContext != null) { + clientBuilder.sslContext(sslContext); } client = clientBuilder.build(); } else { @@ -191,6 +205,30 @@ protected String getJdkMajorVersion() { } } + private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI uri, DynamicLinkAuthenticationSessionRequest request) { + try { + return postRequest(uri, request, DynamicLinkAuthenticationSessionResponse.class); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException e) { + logger.warn("No permission to issue the request", e); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); + } + } + + private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI uri, CertificateRequest request) { + try { + return postRequest(uri, request, DynamicLinkCertificateChoiceSessionResponse.class); + } catch (NotFoundException ex) { + logger.warn("User account not found for URI {}", uri, ex); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } + } + private T postRequest(URI uri, V request, Class responseType) { try { Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); @@ -234,22 +272,8 @@ private SessionStatusRequest createSessionStatusRequest(String sessionId) { private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { if (request.isResponseSocketOpenTimeSet()) { - TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); - long timeValue = request.getResponseSocketOpenTimeValue(); - long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); + long queryTimeoutInMilliseconds = sessionStatusResponseSocketOpenTimeUnit.toMillis(sessionStatusResponseSocketOpenTimeValue); uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); } } - - private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI uri, DynamicLinkAuthenticationSessionRequest request) { - try { - return postRequest(uri, request, DynamicLinkAuthenticationSessionResponse.class); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java new file mode 100644 index 00000000..549307d8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java @@ -0,0 +1,98 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public class CertificateRequest implements Serializable { + + private String relyingPartyUUID; + private String relyingPartyName; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String certificateLevel; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String nonce; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Set capabilities; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private RequestProperties requestProperties; + + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + public String getRelyingPartyName() { + return relyingPartyName; + } + + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public Set getCapabilities() { + return capabilities; + } + + public void setCapabilities(Set capabilities) { + this.capabilities = capabilities; + } + + public RequestProperties getRequestProperties() { + return requestProperties; + } + + public void setRequestProperties(RequestProperties requestProperties) { + this.requestProperties = requestProperties; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java new file mode 100644 index 00000000..97da34a7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java @@ -0,0 +1,60 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class DynamicLinkCertificateChoiceSessionResponse implements Serializable { + + private String sessionID; + private String sessionToken; + private String sessionSecret; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public String getSessionToken() { + return sessionToken; + } + + public void setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + } + + public String getSessionSecret() { + return sessionSecret; + } + + public void setSessionSecret(String sessionSecret) { + this.sessionSecret = sessionSecret; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java new file mode 100644 index 00000000..c34f1119 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -0,0 +1,182 @@ +package ee.sk.smartid.v3.service; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.util.StringUtil.isEmpty; + +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.CertificateLevel; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.CertificateRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.RequestProperties; + +public class DynamicLinkCertificateChoiceSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(DynamicLinkCertificateChoiceSessionRequestBuilder.class); + + private final SmartIdConnector connector; + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private boolean shareMdClientIpAddress; + + /** + * Constructs a new DynamicLinkCertificateRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public void withCapabilities(Set capabilities) { + this.capabilities = capabilities; + } + + /** + * Ask to return the IP address of the mobile device where Smart-ID app was running. + * + * @return this builder + * @see Mobile Device IP sharing + */ + public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Starts a dynamic link-based certificate choice session and returns the session response. + * This response includes essential values such as sessionID, sessionToken, and sessionSecret, + * which can be used by the Relying Party to manage and verify the session independently. + *

        + * @return DynamicLinkCertificateChoiceSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. + * @throws SmartIdClientException if the response is invalid or missing necessary session data. + */ + public DynamicLinkCertificateChoiceSessionResponse initiateCertificateChoice() { + validateParameters(); + CertificateRequest request = createCertificateRequest(); + DynamicLinkCertificateChoiceSessionResponse response = connector.getCertificate(request); + + if (response == null || response.getSessionID() == null) { + throw new SmartIdClientException("Dynamic link certificate choice session failed: invalid response received."); + } + return response; + } + + private void validateParameters() { + if (isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { + throw new SmartIdClientException("Nonce must be between 1 and 30 characters. You supplied: '" + nonce + "'"); + } + } + + private CertificateRequest createCertificateRequest() { + var request = new CertificateRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + request.setNonce(nonce); + request.setCapabilities(capabilities); + + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + if (requestProperties.hasProperties()) { + request.setRequestProperties(requestProperties); + } + + return request; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java index 6d717b20..df3e8d8e 100644 --- a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java +++ b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -40,9 +40,6 @@ import ee.sk.smartid.CertificateParser; import ee.sk.smartid.HashType; -import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.SignableData; -import ee.sk.smartid.v3.SignableHash; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; @@ -56,6 +53,10 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.CertificateLevel; +import ee.sk.smartid.v3.SignableData; +import ee.sk.smartid.v3.SignableHash; import ee.sk.smartid.v3.SmartIdAuthenticationResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -179,7 +180,7 @@ private void validateCertificate(SessionCertificate sessionCertificate, String r X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); cert.checkValidity(); - if (!requestedCertificateLevel.equals(sessionCertificate.getCertificateLevel())) { + if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { throw new CertificateLevelMismatchException(); } @@ -188,12 +189,19 @@ private void validateCertificate(SessionCertificate sessionCertificate, String r } } + private boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { + CertificateLevel requestedLevelEnum = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); + CertificateLevel returnedLevelEnum = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); + + return requestedLevelEnum == CertificateLevel.QSCD ? returnedLevelEnum == CertificateLevel.QUALIFIED : requestedLevelEnum == returnedLevelEnum; + } + private void validateSignature(SessionStatus sessionStatus, String expectedDigest, String randomChallenge) { String signatureProtocol = sessionStatus.getSignatureProtocol(); - if (SignatureProtocol.ACSP_V1.name().equals(signatureProtocol)) { + if (SignatureProtocol.ACSP_V1.name().equalsIgnoreCase(signatureProtocol)) { validateAcspV1Signature(sessionStatus, randomChallenge); - } else if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equals(signatureProtocol)) { + } else if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { validateRawDigestSignature(sessionStatus, expectedDigest); } else { throw new SmartIdClientException("Unknown signature protocol: " + signatureProtocol); @@ -231,7 +239,7 @@ private void validateRawDigestSignature(SessionStatus sessionStatus, String expe + ", but got: " + signatureValue); } - Set allowedSignatureAlgorithms = Set.of("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); + List allowedSignatureAlgorithms = Arrays.asList("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); if (!allowedSignatureAlgorithms.contains(signatureAlgorithm)) { throw new SmartIdClientException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signatureAlgorithm); } diff --git a/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java rename to src/test/java/ee/sk/smartid/DigestCalculatorTest.java index e2e3caa4..eed4a309 100644 --- a/src/test/java/ee/sk/smartid/v2/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -36,7 +36,6 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.HashType; public class DigestCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java index 0164ff42..14a5fc74 100644 --- a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index fb454a3b..ad1d6c5c 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -52,6 +52,15 @@ public static void stubNotFoundResponse(String urlEquals) { .withBody("Not found"))); } + public static void stubPostRequestWithResponse(String url, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + public static void stubNotFoundResponse(String url, String requestFile) { stubErrorResponse(url, requestFile, 404); } @@ -111,6 +120,15 @@ public static void stubSessionStatusWithState(String sessionId, String responseF ); } + public static void stubPostErrorResponse(String url, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody(""))); + } + private static String readFileBody(String fileName) { ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); URL resource = classLoader.getResource(fileName); diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java similarity index 95% rename from src/test/java/ee/sk/smartid/SignableHashTest.java rename to src/test/java/ee/sk/smartid/v2/SignableHashTest.java index 6bfab713..e431405c 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.v2; /*- * #%L @@ -30,8 +30,9 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashType; import ee.sk.smartid.v2.SignableHash; -import ee.sk.smartid.v2.DigestCalculator; public class SignableHashTest { diff --git a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java index 63454762..68828b8e 100644 --- a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java @@ -33,6 +33,7 @@ import org.junit.jupiter.api.Test; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index e30de571..28944673 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -43,7 +43,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.DigestCalculator; +import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; diff --git a/src/test/java/ee/sk/smartid/v3/FileUtil.java b/src/test/java/ee/sk/smartid/v3/FileUtil.java new file mode 100644 index 00000000..d2137b66 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/FileUtil.java @@ -0,0 +1,55 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public final class FileUtil { + + private FileUtil() { + } + + public static String readFileToString(String fileName) { + return new String(readFileBytes(fileName), StandardCharsets.UTF_8); + } + + private static byte[] readFileBytes(String fileName) { + try { + ClassLoader classLoader = FileUtil.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + return Files.readAllBytes(Paths.get(resource.toURI())); + } catch (Exception e) { + throw new RuntimeException("Exception: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index d8283b63..6ad09a7a 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,6 @@ * #L% */ - import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; @@ -37,12 +36,12 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.SmartIdRestServiceStubs; -import ee.sk.smartid.FileUtil; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @WireMockTest(httpPort = 18089) -public class SmartIdClientTest { +class SmartIdClientTest { private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); @@ -57,6 +56,23 @@ void setUp() { smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); } + @Test + void createDynamicLinkCertificateChoice() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + + DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initiateCertificateChoice(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + @Test void createDynamicLinkAuthentication_anonymous() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); @@ -106,4 +122,4 @@ void createDynamicLinkAuthentication_withSemanticsIdentifier() { assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } -} +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 9ca309de..3da46e48 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -31,6 +31,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringStartsWith.startsWith; @@ -50,10 +52,16 @@ import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.SignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.CertificateRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; @@ -61,14 +69,14 @@ class SmartIdRestConnectorTest { @Nested - @WireMockTest(httpPort = 18081) + @WireMockTest(httpPort = 18089) class SessionStatusTests { private SmartIdRestConnector connector; @BeforeEach void setUp() { - connector = new SmartIdRestConnector("http://localhost:18081"); + connector = new SmartIdRestConnector("http://localhost:18089"); } @Test @@ -191,6 +199,117 @@ private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { } } + @Nested + @WireMockTest(httpPort = 18089) + class CertificateChoiceTests { + + private SmartIdConnector connector; + + @BeforeEach + public void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void getCertificate() { + stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v2/responses/dynamicLinkCertificateChoiceResponse.json"); + + CertificateRequest request = createCertificateRequest(); + DynamicLinkCertificateChoiceSessionResponse response = connector.getCertificate(request); + + assertNotNull(response); + assertEquals("de305d54-75b4-431b-adb2-eb6b9e546016", response.getSessionID()); + assertEquals("session-token-value", response.getSessionToken()); + assertEquals("session-secret-value", response.getSessionSecret()); + } + + @Test + void getCertificate_invalidCertificateLevel_throwsBadRequestException() { + CertificateRequest request = createCertificateRequest(); + request.setCertificateLevel("INVALID_LEVEL"); + + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); + + assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_userAccountNotFound() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 404); + + CertificateRequest request = createCertificateRequest(); + assertThrows(UserAccountNotFoundException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_relyingPartyNoPermission() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 403); + + CertificateRequest request = createCertificateRequest(); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_invalidRequest() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); + + CertificateRequest request = new CertificateRequest(); + request.setRelyingPartyUUID(""); + request.setRelyingPartyName(""); + + assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 401); + + CertificateRequest request = createCertificateRequest(); + + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); + + assertEquals("Request is unauthorized for URI http://localhost:18089/certificatechoice/dynamic-link/anonymous", exception.getMessage()); + } + + @Test + void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 471); + + CertificateRequest request = createCertificateRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 472); + + CertificateRequest request = createCertificateRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.getCertificate(request)); + } + + @Test + void getCertificate_throwsSmartIdClientException() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 480); + + CertificateRequest request = createCertificateRequest(); + + Exception exception = assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); + } + + @Test + void getCertificate_throwsServerMaintenanceException() { + stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 580); + + CertificateRequest request = createCertificateRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.getCertificate(request)); + } + } + @Nested @WireMockTest(httpPort = 18081) class AnonymousDynamicLinkAuthentication { @@ -314,4 +433,12 @@ private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessi return dynamicLinkAuthenticationSessionRequest; } -} + + private CertificateRequest createCertificateRequest() { + var request = new CertificateRequest(); + request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + request.setRelyingPartyName("BANK123"); + request.setCertificateLevel("ADVANCED"); + return request; + } +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java new file mode 100644 index 00000000..7285ff9d --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -0,0 +1,216 @@ +package ee.sk.smartid.v3.service; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.v3.CertificateLevel; +import ee.sk.smartid.v3.rest.SessionStatusPoller; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.CertificateRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +class DynamicLinkCertificateChoiceSessionRequestBuilderTest { + + private SmartIdConnector connector; + private SessionStatusPoller sessionStatusPoller; + private DynamicLinkCertificateChoiceSessionRequestBuilder builderService; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + sessionStatusPoller = mock(SessionStatusPoller.class); + + builderService = new DynamicLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890"); + } + + @Test + void initiateCertificateChoice() { + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + + verify(connector).getCertificate(any(CertificateRequest.class)); + } + + @Test + void initiateCertificateChoice_nullRequestProperties() { + builderService.withShareMdClientIpAddress(false); + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + + DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + + assertNotNull(result); + verify(connector).getCertificate(any(CertificateRequest.class)); + } + + @Test + void initiateCertificateChoice_missingCertificateLevel() { + builderService.withCertificateLevel(null); + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + + DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + + assertNotNull(result); + verify(connector).getCertificate(any(CertificateRequest.class)); + } + + @Test + void initiateCertificateChoice_withValidCapabilities() { + Set capabilities = Set.of("SIGN", "AUTH"); + builderService.withCapabilities(capabilities); + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + + verify(connector).getCertificate(any(CertificateRequest.class)); + } + + @Test + void initiateCertificateChoice_nullCapabilities() { + builderService.withCapabilities(null); + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + + DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + + assertNotNull(result); + verify(connector).getCertificate(any(CertificateRequest.class)); + } + + @Nested + class ErrorCases { + + @Test + void initiateCertificateChoice_whenResponseIsNull() { + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_whenSessionIDIsNull() { + var responseWithNullSessionID = new DynamicLinkCertificateChoiceSessionResponse(); + responseWithNullSessionID.setSessionToken("test-session-token"); + responseWithNullSessionID.setSessionSecret("test-session-secret"); + when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(responseWithNullSessionID); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_userAccountNotFound() { + when(connector.getCertificate(any(CertificateRequest.class))).thenThrow(new UserAccountNotFoundException()); + + assertThrows(UserAccountNotFoundException.class, () -> builderService.initiateCertificateChoice()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyUUID() { + builderService.withRelyingPartyUUID(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyName() { + builderService.withRelyingPartyName(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_invalidNonce() { + builderService.withNonce("1234567890123456789012345678901"); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters. You supplied: '1234567890123456789012345678901'", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_emptyNonce() { + builderService.withNonce(""); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters. You supplied: ''", ex.getMessage()); + } + } + + private static DynamicLinkCertificateChoiceSessionResponse mockCertificateChoiceResponse() { + var response = new DynamicLinkCertificateChoiceSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + return response; + } + + private SessionStatus createSessionStatus() { + var sessionStatus = new SessionStatus(); + var sessionResult = new SessionResult(); + + sessionResult.setEndResult("OK"); + sessionStatus.setResult(sessionResult); + + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java b/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java index 8589b7ba..89b227f3 100644 --- a/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java +++ b/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,6 @@ * #L% */ -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -38,14 +37,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,6 +53,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; @@ -139,143 +133,109 @@ void documentFetchingSessionStatus() { } @Test - void documentValidatingSessionStatus() throws Exception { - SmartIdConnector connector = mock(SmartIdConnector.class); - - String randomChallenge = "randomChallenge"; - SessionStatus mockSessionStatus = createMockSessionStatus(randomChallenge); - - when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); - - var poller = new SessionStatusPoller(connector); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); - - SmartIdRequestBuilderService requestBuilder = new SmartIdRequestBuilderService(); - - byte[] dataToSignBytes = "dataToBeSigned".getBytes(StandardCharsets.UTF_8); - var signableData = new SignableData(dataToSignBytes); - signableData.setHashType(HashType.SHA512); + void createSmartIdAuthenticationResponse_validSessionStatus() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - Field dataToSignField = SmartIdRequestBuilderService.class.getDeclaredField("dataToSign"); - dataToSignField.setAccessible(true); - dataToSignField.set(requestBuilder, signableData); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", null, randomChallenge); - - SmartIdAuthenticationResponse response = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, randomChallenge); - - assertEquals("OK", response.getEndResult()); + SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); assertEquals("QUALIFIED", response.getCertificateLevel()); + assertEquals("OK", response.getEndResult()); } @Test - void validateSessionResult_missingInteractionFlowUsed() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); - method.setAccessible(true); - - var sessionStatus = new SessionStatus(); - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); + void createSmartIdAuthenticationResponse_sessionResultNull() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setResult(null); - sessionStatus.setResult(sessionResult); - sessionStatus.setInteractionFlowUsed(null); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("InteractionFlowUsed is missing in the session status", ex.getCause().getMessage()); + assertEquals("Result is missing in the session status response", ex.getMessage()); } @Test - void validateSessionResult_nullSessionResult() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_missingInteractionFlowUsed() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.setInteractionFlowUsed(null); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(null); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Result is missing in the session status response", ex.getCause().getMessage()); + assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); } @Test - void validateSessionResult_missingDocumentNumber() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSessionResult", SessionStatus.class, String.class, String.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_missingDocumentNumber() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.getResult().setDocumentNumber(null); - var sessionStatus = new SessionStatus(); - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber(null); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - sessionStatus.setResult(sessionResult); - sessionStatus.setInteractionFlowUsed("someInteraction"); - - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Document number is missing in the session result", ex.getCause().getMessage()); + assertEquals("Document number is missing in the session result", ex.getMessage()); } @Nested class CertificateValidation { @Test - void validateCertificate_missingCertificate() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_missingCertificate() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.setCert(null); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, null, "QUALIFIED")); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Missing certificate in session response", ex.getCause().getMessage()); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + + assertEquals("Missing certificate in session response", ex.getMessage()); } @Test - void validateCertificate_missingCertificateValue() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_missingCertificateValue() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.getCert().setValue(null); - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(null); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Missing certificate in session response", ex.getCause().getMessage()); + assertEquals("Missing certificate in session response", ex.getMessage()); } @Test - void validateCertificate_certificateLevelMismatch() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_certificateLevelMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.getCert().setCertificateLevel("ADVANCED"); - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); - sessionCertificate.setCertificateLevel("ADVANCED"); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertTrue(ex.getCause().getCause() instanceof CertificateLevelMismatchException); + assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); } @Test - void validateCertificate_certificateParsingFails() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateCertificate", SessionCertificate.class, String.class); - method.setAccessible(true); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("InvalidCertificateData"); - sessionCertificate.setCertificateLevel("QUALIFIED"); + void createSmartIdAuthenticationResponse_withQscdRequestedAndQualifiedReturned() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.getCert().setCertificateLevel("QUALIFIED"); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionCertificate, "QUALIFIED")); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); + service.certificateLevel = "QSCD"; - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Certificate validation failed", ex.getCause().getMessage()); + SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QSCD", "expectedDigest", "randomChallenge"); + assertEquals("QUALIFIED", response.getCertificateLevel()); } } @@ -283,173 +243,138 @@ void validateCertificate_certificateParsingFails() throws Exception { class SignatureValidation { @Test - void validateRawDigestSignature_successful() throws Exception { - SessionStatus mockSessionStatus = mock(SessionStatus.class); - SessionSignature mockSessionSignature = mock(SessionSignature.class); - - when(mockSessionSignature.getValue()).thenReturn("expectedDigest"); - when(mockSessionSignature.getSignatureAlgorithm()).thenReturn("sha512WithRSAEncryption"); - when(mockSessionStatus.getSignature()).thenReturn(mockSessionSignature); + void createSmartIdAuthenticationResponse_validRawDigestSignature() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); - method.setAccessible(true); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - assertDoesNotThrow(() -> method.invoke(service, mockSessionStatus, "expectedDigest")); + SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); + assertEquals("OK", response.getEndResult()); } @Test - void validateSignature_withRawDigestSignatureProtocol() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSignature", SessionStatus.class, String.class, String.class); - method.setAccessible(true); - - var sessionStatus = new SessionStatus(); + void createSmartIdAuthenticationResponse_rawDigestSignatureMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("wrongDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("expectedDigest"); - sessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); - sessionStatus.setSignature(sessionSignature); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); + + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertDoesNotThrow(() -> method.invoke(service, sessionStatus, "expectedDigest", "randomChallenge")); + assertTrue(ex.getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); } @Test - void validateSignature_unknownProtocol() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateSignature", SessionStatus.class, String.class, String.class); - method.setAccessible(true); + void createSmartIdAuthenticationResponse_rawDigestUnexpectedAlgorithm() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm", null); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - var sessionStatus = new SessionStatus(); - sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest", "randomChallenge")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getCause().getMessage()); + assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); } @Test - void validateAcspV1Signature_signatureMismatch() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateAcspV1Signature", SessionStatus.class, String.class); - method.setAccessible(true); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("InvalidSignatureValue"); - sessionSignature.setServerRandom("serverRandomValue"); + void createSmartIdAuthenticationResponse_acspV1SignatureMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("wrongSignatureValue", "ACSP_V1", + "sha512WithRSAEncryption", "SHA-512"); - var sigAlgParams = new SignatureAlgorithmParameters(); - sigAlgParams.setHashAlgorithm("SHA-256"); - sessionSignature.setSignatureAlgorithmParameters(sigAlgParams); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var sessionStatus = new SessionStatus(); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol("ACSP_V1"); + SmartIdClientException ex = assertThrows(SmartIdClientException.class, () -> + service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, "randomChallenge")); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "randomChallenge")); - - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertTrue(ex.getCause().getMessage().contains("ACSP_V1 signature validation failed")); + assertTrue(ex.getMessage().contains("ACSP_V1 signature validation failed")); } @Test - void validateRawDigestSignature_signatureMismatch() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); - method.setAccessible(true); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("actualDigest"); - sessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); + void createSmartIdAuthenticationResponse_acspV1NoSuchAlgorithmException() { + SessionStatus sessionStatus = createMockSessionStatus(null, "ACSP_V1", + "sha512WithRSAEncryption", "INVALID_ALGORITHM"); - var sessionStatus = new SessionStatus(); - sessionStatus.setSignature(sessionSignature); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest")); + SmartIdClientException ex = assertThrows(SmartIdClientException.class, () -> + service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertTrue(ex.getCause().getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); + assertEquals("Error while creating digest for ACSP_V1 signature validation", ex.getMessage()); } @Test - void validateRawDigestSignature_unexpectedAlgorithm() throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("validateRawDigestSignature", SessionStatus.class, String.class); - method.setAccessible(true); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("expectedDigest"); - sessionSignature.setSignatureAlgorithm("unexpectedAlgorithm"); + void createSmartIdAuthenticationResponse_unknownSignatureProtocol() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "UNKNOWN_PROTOCOL", "sha512WithRSAEncryption", null); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - var sessionStatus = new SessionStatus(); - sessionStatus.setSignature(sessionSignature); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, sessionStatus, "expectedDigest")); + var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertTrue(ex.getCause() instanceof SmartIdClientException); - assertTrue(ex.getCause().getMessage().contains("Unexpected signature algorithm")); + assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); } - } - @Test - void documentFetchingSessionStatus_mocked() { - SmartIdConnector connector = mock(SmartIdConnector.class); - var mockSessionStatus = new SessionStatus(); - mockSessionStatus.setState("COMPLETE"); + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void createSmartIdAuthenticationResponse_handleSessionEndResultErrors(String endResult, Class expectedException) { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); + sessionStatus.getResult().setEndResult(endResult); - when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); - - var poller = new SessionStatusPoller(connector); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - assertEquals("COMPLETE", sessionStatus.getState()); - } + assertThrows(expectedException, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); + } - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void handleSessionEndResultErrors(String endResult, Class expectedException) throws Exception { - Method method = SmartIdRequestBuilderService.class.getDeclaredMethod("handleSessionEndResultErrors", String.class); - method.setAccessible(true); + @Test + void createSmartIdAuthenticationResponse_sessionStatusNull() { + service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); + service.dataToSign.setHashType(HashType.SHA512); - var ex = assertThrows(InvocationTargetException.class, () -> method.invoke(service, endResult)); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> service.createSmartIdAuthenticationResponse(null, "QUALIFIED", "expectedDigest", "randomChallenge")); - assertEquals(expectedException, ex.getCause().getClass()); + assertEquals("Session status is null", ex.getMessage()); + } } - private static SessionStatus createMockSessionStatus(String randomChallenge) throws NoSuchAlgorithmException { - var mockSessionResult = new SessionResult(); - mockSessionResult.setEndResult("OK"); - mockSessionResult.setDocumentNumber("PNOEE-12345678901"); + private static SessionStatus createMockSessionStatus(String signatureValue, String signatureProtocol, String signatureAlgorithm, String hashAlgorithm) { - var mockCertificate = new SessionCertificate(); - mockCertificate.setCertificateLevel("QUALIFIED"); - mockCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); - - var mockSessionSignature = new SessionSignature(); - String serverRandom = "serverRandomValue"; - String signatureProtocol = "ACSP_V1"; - - String dataToHash = signatureProtocol + ";" + - Base64.getEncoder().encodeToString(serverRandom.getBytes(StandardCharsets.UTF_8)) + ";" + - Base64.getEncoder().encodeToString(randomChallenge.getBytes(StandardCharsets.UTF_8)); + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); - var sigAlgParams = new SignatureAlgorithmParameters(); - sigAlgParams.setHashAlgorithm("SHA-512"); - mockSessionSignature.setSignatureAlgorithmParameters(sigAlgParams); + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); - MessageDigest digest = MessageDigest.getInstance(sigAlgParams.getHashAlgorithm()); - byte[] hashedData = digest.digest(dataToHash.getBytes(StandardCharsets.UTF_8)); - String expectedSignature = Base64.getEncoder().encodeToString(hashedData); + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setServerRandom("serverRandomValue"); - mockSessionSignature.setValue(expectedSignature); - mockSessionSignature.setServerRandom(serverRandom); - mockSessionSignature.setSignatureAlgorithm("sha512WithRSAEncryption"); + if ("ACSP_V1".equals(signatureProtocol)) { + var sigAlgParams = new SignatureAlgorithmParameters(); + sigAlgParams.setHashAlgorithm(hashAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(sigAlgParams); + } - var mockSessionStatus = new SessionStatus(); - mockSessionStatus.setState("COMPLETE"); - mockSessionStatus.setResult(mockSessionResult); - mockSessionStatus.setCert(mockCertificate); - mockSessionStatus.setSignature(mockSessionSignature); - mockSessionStatus.setSignatureProtocol(signatureProtocol); - mockSessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(signatureProtocol); + sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); - return mockSessionStatus; + return sessionStatus; } static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { diff --git a/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json b/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json new file mode 100644 index 00000000..0f1fdbd8 --- /dev/null +++ b/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json @@ -0,0 +1,5 @@ +{ + "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546016", + "sessionToken": "session-token-value", + "sessionSecret": "session-secret-value" +} diff --git a/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json b/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json new file mode 100644 index 00000000..098a1060 --- /dev/null +++ b/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json @@ -0,0 +1,9 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED", + "nonce": "cmFuZG9tTm9uY2U=", + "requestProperties": { + "shareMdClientIpAddress": false + } +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json b/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json new file mode 100644 index 00000000..a6cbb77c --- /dev/null +++ b/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json @@ -0,0 +1,5 @@ +{ + "sessionID": "abcdef1234567890", + "sessionToken": "sampleSessionToken", + "sessionSecret": "sampleSessionSecret" +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/session-status-not-ok.json b/src/test/resources/v3/responses/session-status-not-ok.json new file mode 100644 index 00000000..6b345d36 --- /dev/null +++ b/src/test/resources/v3/responses/session-status-not-ok.json @@ -0,0 +1,6 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "NOT-OK" + } +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/session-status-ok.json b/src/test/resources/v3/responses/session-status-ok.json new file mode 100644 index 00000000..6fcaa47b --- /dev/null +++ b/src/test/resources/v3/responses/session-status-ok.json @@ -0,0 +1,6 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "OK" + } +} \ No newline at end of file From df158696e9f29a037e919f021f07d446dfb6472a Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:02:47 +0200 Subject: [PATCH 07/57] SLIB-53 - added dynamic link based signature request handling (#91) * SLIB-53 - added dynamic link based signature request handling * SLIB-53 - refactored signatureProtocolParameters, fixed tests, * SLIB-53 - added ResponseParameters validation * SLIB-53 - fixed testnames * SLIB-53 - fixed tests, removed unnecessary code * SLIB-53 - refacored testklasses, fixed README * SLIB-53 - fixed README, tests and refactored default signatureAlgorithm handling * SLIB-53 - fixed testname, removed unneseccary part from README --------- Co-authored-by: ragnar.haide --- README.md | 308 ++++++++---- ...=> AcspV1SignatureProtocolParameters.java} | 2 +- ...namicLinkAuthenticationSessionRequest.java | 10 +- ...nkAuthenticationSessionRequestBuilder.java | 2 +- .../DynamicLinkSignatureSessionRequest.java | 128 +++++ ...micLinkSignatureSessionRequestBuilder.java | 367 ++++++++++++++ .../DynamicLinkSignatureSessionResponse.java | 63 +++ ...RawDigestSignatureProtocolParameters.java} | 33 +- .../ee/sk/smartid/v3/SignatureProtocol.java | 3 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 19 + .../sk/smartid/v3/rest/SmartIdConnector.java | 20 + .../smartid/v3/rest/SmartIdRestConnector.java | 84 +++- .../service/SmartIdRequestBuilderService.java | 2 +- .../ee/sk/smartid/integration/ReadmeTest.java | 10 +- .../integration/SmartIdIntegrationTest.java | 20 +- ...ndpointSslVerificationIntegrationTest.java | 2 +- .../v2/rest/SmartIdRestIntegrationTest.java | 6 +- ...inkSignatureSessionRequestBuilderTest.java | 472 ++++++++++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 45 ++ .../v3/rest/SmartIdRestConnectorTest.java | 282 ++++++++--- .../v3/rest/SmartIdRestIntegrationTest.java | 4 +- .../dynamicLinkSignatureResponse.json | 5 + ...c-link-authentication-session-request.json | 4 +- .../dynamic-link-signature-request.json | 18 + .../dynamic-link-signature-response.json | 5 + 25 files changed, 1701 insertions(+), 213 deletions(-) rename src/main/java/ee/sk/smartid/v3/{SignatureProtocolParameters.java => AcspV1SignatureProtocolParameters.java} (95%) create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java rename src/main/java/ee/sk/smartid/v3/{rest/dao/SignatureProtocol.java => RawDigestSignatureProtocolParameters.java} (67%) create mode 100644 src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java create mode 100644 src/test/resources/v2/responses/dynamicLinkSignatureResponse.json create mode 100644 src/test/resources/v3/requests/dynamic-link-signature-request.json create mode 100644 src/test/resources/v3/responses/dynamic-link-signature-response.json diff --git a/README.md b/README.md index 813341f0..329a6dc0 100644 --- a/README.md +++ b/README.md @@ -784,14 +784,6 @@ Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) The Smart-ID v3.0 API includes new session status request paths for retrieving session results. -## Session status endpoint -* Method: `GET` -* Path: `BASE/v3/session/:sessionId` -* Query parameter: `timeoutMs` (optional, long poll timeout value, default is halfway between max(120000ms) and min(1000ms) values) - -Example of the endpoint: -https://rp-api.smart-id.com/v3/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000 - ## Session status response The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: @@ -807,63 +799,6 @@ The session status response includes various fields depending on whether the ses * `interactionFlowUsed`: The interaction flow used for the session. * `deviceIpAddress`: IP address of the mobile device, if requested. -### Successful response when still waiting for user’s response - -```json -{ - "state": "RUNNING" -} -``` - -### ACSP_V1 is returned in the session status OK response for authentication sessions in both dynamic-link and notification-based flows. - -```json - { - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-372...." - }, - "signatureProtocol": "ACSP_V1", - "signature": { - "serverRandom": "B+C9XVjIAZnCHH9vfBSv...", - "value": "B+A9CfjIBZnDHHav3B4F...", - "signatureAlgorithm": "sha512WithRSAEncryption", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "cert": { - "value": "B+C9XVjIAZnCHH9vfBSv...", - "certificateLevel": "QUALIFIED" - } -} -``` - -### RAW_DIGEST_SIGNATURE is returned in the session status OK response for signature sessions in both dynamic link and notification-based flows. - -```json -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-372...." - }, - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signature": { - "value": "B+A9CfjIBZnDHHav3B4F...", - "signatureAlgorithm": "sha512WithRSAEncryption", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "cert": { - "value": "B+C9XVjIAZnCHH9vfBSv...", - "certificateLevel": "QUALIFIED" - } -} -``` - ## Example of fetching session status in v3.0 The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. @@ -940,16 +875,6 @@ try { The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a certificate choice session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the authentication process. -## Dynamic Link Certificate Choice Endpoint - -To initiate a dynamic link certificate choice session, send a POST request to the following endpoint: - -* Method: `POST` -* Path: `BASE/v3/certificatechoice/dynamic-link/anonymous` - -Example of the endpoint: -https://rp-api.smart-id.com/v3/certificatechoice/dynamic-link/anonymous - ## Request Parameters The request parameters for the dynamic link certificate choice session are: @@ -981,22 +906,13 @@ DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkC // Note: After a certificate choice request, a notification-based signature choice must follow. ``` -## Response on Successful Session Creation +## Response on Successful Certificate Choice Session Creation The response from a successful dynamic link certificate choice session creation contains the following parameters: * `sessionID`: A string that can be used to request the operation result. * `sessionToken`: Unique random value that will be used to connect this certificate choice attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -### Example of a Successful Response -```json -{ - "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546014", - "sessionToken": "hyBdQYUeQtvXEPqWC7K8a97L", - "sessionSecret": "dztL7Ur49D/YYgUzYl4sMg==" -} -``` - ## Fetching Session Status After initiating the dynamic link certificate choice session and storing the session information, you can fetch the session status to check if the user has completed the authentication process. @@ -1065,5 +981,227 @@ SmartIdClient client = new SmartIdClient(); .initiateCertificateChoice(); ``` +# Initiating a Dynamic Link Signature Session in API v3.0 +The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a signature session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the signing process using a dynamic link. The user can then access the link and complete the signing process. + +## Request Parameters +The request parameters for the dynamic link signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. + * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. +* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +## Examples of Allowed Interactions Order +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. Different interactions can support different amounts of data to display information to the user. + +Below are examples of `allowedInteractionsOrder` elements in JSON format: +Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessage("Up to 200 characters of text here.."), + Interaction.displayTextAndPIN("Up to 60 characters of text here..") + )); +``` + +Example 2: `confirmationMessage` with Fallback to `verificationCodeChoice` +Description: The RP's first choice is `confirmationMessage`; if not available, then use `verificationCodeChoice`. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessage("Up to 200 characters of text here.."), + Interaction.verificationCodeChoice("Up to 60 characters of text here..") +)); +``` + +Example 3: `displayTextAndPIN` Only +Description: Use `displayTextAndPIN` interaction only. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.displayTextAndPIN("Up to 60 characters of text here..") +)); +``` + +Example 4: `verificationCodeChoice` with Fallback to `displayTextAndPIN` +Description: Use `verificationCodeChoice`; if not available, then `displayTextAndPIN` should be used. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.verificationCodeChoice("Up to 60 characters of text here.."), + Interaction.displayTextAndPIN("Up to 60 characters of text here..") + )); +``` + +Example 5: `confirmationMessage` Only (No Fallback) +Description: Insist on `confirmationMessage`; if not available, then fail. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessage("Up to 200 characters of text here..") +)); +``` + +## Using Semantics Identifier +```java +var client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Create the signable data +var signableData = new SignableData("Test data to sign".getBytes()); +signableData.setHashType(HashType.SHA256); + +// Create the Semantics Identifier +var semanticsIdentifier = new SemanticsIdentifier( +SemanticsIdentifier.IdentityType.PNO, +SemanticsIdentifier.CountryCode.EE,"31111111111"); + +// Build the dynamic link signature request +var builder = client.createDynamicLinkSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); + +// Initiate the dynamic link signature +DynamicLinkSignatureSessionResponse signatureResponse = builder.initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.getSessionID(); +String sessionToken = signatureResponse.getSessionToken(); +String sessionSecret = signatureResponse.getSessionSecret(); + +// Use the session information as needed +``` + +## Using Document Number +```java +SmartIdClient client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Create the signable data +var signableData = new SignableData("Test data to sign".getBytes()); +signableData.setHashType(HashType.SHA256); + +// Specify the document number +String documentNumber = "PNOEE-31111111111-MOCK-Q"; + +// Build the dynamic link signature request +var builder = client.createDynamicLinkSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); + +// Initiate the dynamic link signature +DynamicLinkSignatureSessionResponse signatureResponse = builder.initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.getSessionID(); +String sessionToken = signatureResponse.getSessionToken(); +String sessionSecret = signatureResponse.getSessionSecret(); + +// Use the session information as needed +``` + +## Response Parameters +The response from a successful dynamic link signature session creation contains the following parameters: + +* `sessionID`: A string that can be used to request the operation result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. + +## Error Handling +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException`, `SessionTimeoutException`, and others. + +```java +try { +DynamicLinkSignatureSessionResponse response = builder.initSignatureSession(); + +String sessionID = response.getSessionID(); +String sessionToken = response.getSessionToken(); +String sessionSecret = response.getSessionSecret(); + +System.out.println("Session ID: " + sessionID); +System.out.println("Session Token: " + sessionToken); +System.out.println("Session Secret: " + sessionSecret); + +} catch (UserAccountNotFoundException e) { +System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { +System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { +System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { +System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { +System.out.println("An error occurred: " + e.getMessage()); +} +``` + +## Additional Information +* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interaction it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. + +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessage("Please confirm the transaction of 1024.50 EUR"), + Interaction.displayTextAndPIN("Confirm transaction") +)); +``` + +* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. + +```java +var parameters = new RawDigestSignatureProtocolParameters(); +parameters.setDigest(signableData.calculateHashInBase64()); +parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); +parameters.setSignatureAlgorithmParameters(new SignatureAlgorithmParameters("SHA-512")); +builder.withSignatureProtocolParameters(parameters); +``` + +* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. + +```java +var requestProperties = new RequestProperties(); +requestProperties.setShareMdClientIpAddress(true); +builder.withRequestProperties(requestProperties); +``` + +* `Nonce`: A unique identifier (up to 30 characters) used to manage idempotent behavior in session creation requests. If a request is repeated within a 15-second timeframe, the same session ID may be returned unless a different nonce is provided. + +```java +builder.withNonce("randomNonce123"); +``` + +* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. + +```java +builder.withCapabilities(Set.of("QUILIFIED", "ADVANCED")); +``` + +* `Certificate Level`: Set the required certificate level (`ADVANCED` or `QUALIFIED`). Defaults to `QUALIFIED`. + +```java +builder.withCertificateLevel(CertificateLevel.QUALIFIED); +``` + ### Generating QR-code or dynamic link Todo: will be implemented in task SLIB-55 \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java similarity index 95% rename from src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java index 9c1b7424..80d2d659 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java @@ -28,7 +28,7 @@ import java.io.Serializable; -public class SignatureProtocolParameters implements Serializable { +public class AcspV1SignatureProtocolParameters implements Serializable { private String randomChallenge; private String signatureAlgorithm; diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java index 45c514e1..28e26385 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java @@ -45,7 +45,7 @@ public class DynamicLinkAuthenticationSessionRequest implements Serializable { private final SignatureProtocol signatureProtocol = SignatureProtocol.ACSP_V1; - private SignatureProtocolParameters signatureProtocolParameters; + private AcspV1SignatureProtocolParameters acspV1SignatureProtocolParameters; @JsonInclude(JsonInclude.Include.NON_EMPTY) private String nonce; @@ -86,12 +86,12 @@ public SignatureProtocol getSignatureProtocol() { return signatureProtocol; } - public SignatureProtocolParameters getSignatureProtocolParameters() { - return signatureProtocolParameters; + public AcspV1SignatureProtocolParameters getSignatureProtocolParameters() { + return acspV1SignatureProtocolParameters; } - public void setSignatureProtocolParameters(SignatureProtocolParameters signatureProtocolParameters) { - this.signatureProtocolParameters = signatureProtocolParameters; + public void setSignatureProtocolParameters(AcspV1SignatureProtocolParameters acspV1SignatureProtocolParameters) { + this.acspV1SignatureProtocolParameters = acspV1SignatureProtocolParameters; } public String getNonce() { diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java index 816b1dd5..8df73d8e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -304,7 +304,7 @@ private DynamicLinkAuthenticationSessionRequest createAuthenticationRequest() { request.setCertificateLevel(certificateLevel.name()); } - var signatureProtocolParameters = new SignatureProtocolParameters(); + var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); signatureProtocolParameters.setRandomChallenge(randomChallenge); signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); request.setSignatureProtocolParameters(signatureProtocolParameters); diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java new file mode 100644 index 00000000..842af0f8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java @@ -0,0 +1,128 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.RequestProperties; + +public class DynamicLinkSignatureSessionRequest implements Serializable { + + private String relyingPartyUUID; + private String relyingPartyName; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String certificateLevel; + + private final SignatureProtocol signatureProtocol = SignatureProtocol.RAW_DIGEST_SIGNATURE; + + private RawDigestSignatureProtocolParameters signatureProtocolParameters; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String nonce; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Set capabilities; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List allowedInteractionsOrder; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private RequestProperties requestProperties; + + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + public String getRelyingPartyName() { + return relyingPartyName; + } + + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public SignatureProtocol getSignatureProtocol() { + return signatureProtocol; + } + + public RawDigestSignatureProtocolParameters getSignatureProtocolParameters() { + return signatureProtocolParameters; + } + + public void setSignatureProtocolParameters(RawDigestSignatureProtocolParameters signatureProtocolParameters) { + this.signatureProtocolParameters = signatureProtocolParameters; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public Set getCapabilities() { + return capabilities; + } + + public void setCapabilities(Set capabilities) { + this.capabilities = capabilities; + } + + public List getAllowedInteractionsOrder() { + return allowedInteractionsOrder; + } + + public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + } + + public RequestProperties getRequestProperties() { + return requestProperties; + } + + public void setRequestProperties(RequestProperties requestProperties) { + this.requestProperties = requestProperties; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java new file mode 100644 index 00000000..0094a438 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java @@ -0,0 +1,367 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +public class DynamicLinkSignatureSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(DynamicLinkSignatureSessionRequestBuilder.class); + + private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = + Set.of(InteractionFlow.VERIFICATION_CODE_CHOICE, InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List allowedInteractionsOrder; + private boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm; + private SignableData signableData; + private SignableHash signableHash; + private boolean certificateChoiceMade; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public DynamicLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withCapabilities(Set capabilities) { + this.capabilities = capabilities; + return this; + } + + /** + * Sets the allowed interactions order. + * + * @param allowedInteractionsOrder the allowed interactions order + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + return this; + } + + /** + * Ask to return the IP address of the mobile device where Smart-ID app was running. + * + * @return this builder + * @see Mobile Device IP sharing + */ + public DynamicLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

        + * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + * If both {@link SignableData} and {@link SignableHash} are provided, {@link SignableData} will take precedence. + * + * @param signableData the data to be signed + * @return this builder instance + */ + public DynamicLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + this.signableData = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

        + * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + * + * @param signableHash the hash data to be signed + * @return this builder + */ + public DynamicLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + this.signableHash = signableHash; + return this; + } + + /** + * Marks whether a certificate choice has been made. + *

        + * This method allows specifying if a certificate selection was made prior to initiating this signing session. + * Once set to true, the signing request can proceed without further certificate selection. + * + * @param certificateChoiceMade indicates if certificate choice has been made + * @return this builder instance + */ + public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boolean certificateChoiceMade) { + this.certificateChoiceMade = certificateChoiceMade; + return this; + } + + /** + * Sends the signature request and initiates a dynamic link-based signature session. + *

        + * There are two supported ways to start the signature session: + *

          + *
        • with a document number by using {@link #withDocumentNumber(String)}
        • + *
        • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
        • + *
        + * + * @return a {@link DynamicLinkSignatureSessionResponse} containing session details such as + * session ID, session token, and session secret. + */ + public DynamicLinkSignatureSessionResponse initSignatureSession() { + validateParameters(); + DynamicLinkSignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); + DynamicLinkSignatureSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + validateResponseParameters(dynamicLinkSignatureSessionResponse); + return dynamicLinkSignatureSessionResponse; + } + + private DynamicLinkSignatureSessionResponse initSignatureSession(DynamicLinkSignatureSessionRequest request) { + if (documentNumber != null) { + return connector.initDynamicLinkSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initDynamicLinkSignature(request, semanticsIdentifier); + } else { + throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set. Anonymous signing is not allowed."); + } + } + + private DynamicLinkSignatureSessionRequest createSignatureSessionRequest() { + var request = new DynamicLinkSignatureSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); + if (signableHash != null || signableData != null) { + signatureProtocolParameters.setDigest(getDigestToSignBase64()); + } + signatureProtocolParameters.setSignatureAlgorithm(getSignatureAlgorithm()); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setNonce(nonce); + request.setAllowedInteractionsOrder(allowedInteractionsOrder); + + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + if (requestProperties.hasProperties()) { + request.setRequestProperties(requestProperties); + } + request.setCapabilities(capabilities); + return request; + } + + private String getDigestToSignBase64() { + if (signableHash != null && signableHash.areFieldsFilled()) { + return signableHash.getHashInBase64(); + } else if (signableData != null) { + if (signableData.getHashType() == null) { + throw new SmartIdClientException("HashType must be set for signableData."); + } + return signableData.calculateHashInBase64(); + } else { + throw new SmartIdClientException("Either signableHash or signableData must be set."); + } + } + + private String getSignatureAlgorithm() { + if (signatureAlgorithm != null) { + return signatureAlgorithm.getAlgorithmName(); + } else if (signableHash != null && signableHash.getHashType() != null) { + return getSignatureAlgorithmName(signableHash.getHashType()); + } else if (signableData != null && signableData.getHashType() != null) { + return getSignatureAlgorithmName(signableData.getHashType()); + } else { + return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + } + } + + private String getSignatureAlgorithmName(HashType hashType) { + return switch (hashType) { + case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); + case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); + case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + }; + } + + private void validateParameters() { + if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { + throw new SmartIdClientException("Relying Party UUID must be set."); + } + if (relyingPartyName == null || relyingPartyName.isEmpty()) { + throw new SmartIdClientException("Relying Party Name must be set."); + } + validateAllowedInteractions(); + + if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { + throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); + } + if (certificateChoiceMade) { + throw new SmartIdClientException("Certificate choice was made before using this method. Cannot proceed with signature request."); + } + } + + private void validateAllowedInteractions() { + if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); + } + Optional notSupportedInteraction = allowedInteractionsOrder.stream() + .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) + .findFirst(); + if (notSupportedInteraction.isPresent()) { + logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); + throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); + } + allowedInteractionsOrder.forEach(Interaction::validate); + } + + private void validateResponseParameters(DynamicLinkSignatureSessionResponse dynamicLinkSignatureSessionResponse) { + if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + + if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionToken())) { + logger.error("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + } + + if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionSecret())) { + logger.error("Session secret is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java new file mode 100644 index 00000000..0a18b796 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java @@ -0,0 +1,63 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DynamicLinkSignatureSessionResponse implements Serializable { + + private String sessionID; + private String sessionToken; + private String sessionSecret; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public String getSessionToken() { + return sessionToken; + } + + public void setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + } + + public String getSessionSecret() { + return sessionSecret; + } + + public void setSessionSecret(String sessionSecret) { + this.sessionSecret = sessionSecret; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java b/src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java similarity index 67% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java rename to src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java index b6760ee2..eb5cc822 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.v3; /*- * #%L @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,26 @@ * #L% */ -public enum SignatureProtocol { - ACSP_V1, - RAW_DIGEST_SIGNATURE; -} +import java.io.Serializable; + +public class RawDigestSignatureProtocolParameters implements Serializable { + + private String digest; + private String signatureAlgorithm; + + public String getDigest() { + return digest; + } + + public void setDigest(String digest) { + this.digest = digest; + } + + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java b/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java index a8027330..69a341cc 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java @@ -27,5 +27,6 @@ */ public enum SignatureProtocol { - ACSP_V1 + ACSP_V1, + RAW_DIGEST_SIGNATURE } diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 1baa337a..59136259 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -66,14 +66,33 @@ public class SmartIdClient { private SmartIdConnector connector; private SSLContext trustSslContext; + /** + * Creates a new builder for creating a dynamic link certificate choice session request. + * + * @return a builder for creating a new dynamic link certificate choice session request + */ public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertificateRequest() { return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()); } + /** + * Creates a new builder for creating a new dynamic link authentication session request + * + * @return builder for creating a new dynamic link authentication session request + */ public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentication() { return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()); } + /** + * Creates a new builder for creating a new dynamic link signature session request + * + * @return builder for creating a new dynamic link signature session request + */ + public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { + return new DynamicLinkSignatureSessionRequestBuilder(getSmartIdConnector()); + } + /** * Sets the UUID of the relying party *

        diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index aaa9561a..f0fe72ca 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,6 +32,8 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; @@ -66,6 +68,24 @@ public interface SmartIdConnector extends Serializable { */ DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateRequest request); + /** + * Initiates a dynamic link based signature sessions. + * + * @param request DynamicLinkSignatureSessionRequest containing necessary parameters for the signature session + * @param semanticsIdentifier The semantics identifier + * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret + */ + DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a dynamic link based signature sessions. + * + * @param request DynamicLinkSignatureSessionRequest containing necessary parameters for the signature session + * @param documentNumber The document number + * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret + */ + DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, String documentNumber); + /** * Set the SSL context to use for secure communication * diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index dd4829b2..63d1ac3b 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -47,9 +47,11 @@ import ee.sk.smartid.rest.LoggingFilter; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; import jakarta.ws.rs.BadRequestException; @@ -75,6 +77,8 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String SESSION_STATUS_URI = "/session/{sessionId}"; private static final String CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH = "/certificatechoice/dynamic-link/anonymous"; + private static final String DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/dynamic-link/etsi"; + private static final String DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/dynamic-link/document"; private static final String ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH = "authentication/dynamic-link/anonymous"; private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; @@ -154,6 +158,26 @@ public DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateReq return postCertificateRequest(uri, request); } + @Override + public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postSignatureRequest(uri, request); + } + + @Override + public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postSignatureRequest(uri, request); + } + @Override public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; @@ -169,11 +193,11 @@ protected Invocation.Builder prepareClient(URI uri) { Client client; if (this.configuredClient == null) { ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (clientConfig != null) { - clientBuilder.withConfig(clientConfig); + if (null != this.clientConfig) { + clientBuilder.withConfig(this.clientConfig); } - if (sslContext != null) { - clientBuilder.sslContext(sslContext); + if (null != this.sslContext) { + clientBuilder.sslContext(this.sslContext); } client = clientBuilder.build(); } else { @@ -229,36 +253,48 @@ private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI u } } + private DynamicLinkSignatureSessionResponse postSignatureRequest(URI uri, DynamicLinkSignatureSessionRequest request) { + try { + return postRequest(uri, request, DynamicLinkSignatureSessionResponse.class); + } catch (NotFoundException ex) { + logger.warn("User account not found for URI " + uri, ex); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } + } + private T postRequest(URI uri, V request, Class responseType) { try { Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); return prepareClient(uri).post(requestEntity, responseType); - } catch (NotAuthorizedException e) { - logger.warn("Request is unauthorized for URI " + uri, e); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); - } catch (BadRequestException e) { - logger.warn("Request is invalid for URI " + uri, e); - throw new SmartIdClientException("Server refused the request", e); - } catch (ClientErrorException e) { - if (e.getResponse().getStatus() == 471) { - logger.warn("No suitable account of requested type found, but user has some other accounts.", e); + } catch (NotAuthorizedException ex) { + logger.warn("Request is unauthorized for URI {}", uri, ex); + throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, ex); + } catch (BadRequestException ex) { + logger.warn("Request is invalid for URI {}", uri, ex); + throw new SmartIdClientException("Server refused the request", ex); + } catch (ClientErrorException ex) { + if (ex.getResponse().getStatus() == 471) { + logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); throw new NoSuitableAccountOfRequestedTypeFoundException(); } - if (e.getResponse().getStatus() == 472) { - logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", e); + if (ex.getResponse().getStatus() == 472) { + logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", ex); throw new PersonShouldViewSmartIdPortalException(); } - if (e.getResponse().getStatus() == 480) { + if (ex.getResponse().getStatus() == 480) { logger.warn("Client-side API is too old and not supported anymore"); throw new SmartIdClientException("Client-side API is too old and not supported anymore"); } - throw e; - } catch (ServerErrorException e) { - if (e.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", e); + throw ex; + } catch (ServerErrorException ex) { + if (ex.getResponse().getStatus() == 580) { + logger.warn("Server is under maintenance, retry later", ex); throw new ServerMaintenanceException(); } - throw e; + throw ex; } } @@ -272,7 +308,9 @@ private SessionStatusRequest createSessionStatusRequest(String sessionId) { private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { if (request.isResponseSocketOpenTimeSet()) { - long queryTimeoutInMilliseconds = sessionStatusResponseSocketOpenTimeUnit.toMillis(sessionStatusResponseSocketOpenTimeValue); + TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); + long timeValue = request.getResponseSocketOpenTimeValue(); + long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); } } diff --git a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java index df3e8d8e..a22255ca 100644 --- a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java +++ b/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java @@ -57,6 +57,7 @@ import ee.sk.smartid.v3.CertificateLevel; import ee.sk.smartid.v3.SignableData; import ee.sk.smartid.v3.SignableHash; +import ee.sk.smartid.v3.SignatureProtocol; import ee.sk.smartid.v3.SmartIdAuthenticationResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -64,7 +65,6 @@ import ee.sk.smartid.v3.rest.dao.SessionResult; import ee.sk.smartid.v3.rest.dao.SessionSignature; import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SignatureProtocol; public class SmartIdRequestBuilderService { diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java index 74189922..e828b845 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java @@ -391,7 +391,7 @@ public void documentCreatingSignature() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") // returned as authentication result + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(asList( @@ -440,7 +440,7 @@ public void documentCreatingSignature() { public void documentInteractionOrderMostCommon() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(Collections.singletonList( @@ -467,7 +467,7 @@ public void documentInteractionOrderVerificationChoice() { try { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(Arrays.asList( @@ -494,7 +494,7 @@ public void documentInteractionOrderVerificationChoice() { public void documentInteractionOrderConfirmationWithFallbackToPin() { SmartIdSignature smartIdSignature = client .createSignature() - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") // + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(asList( @@ -558,7 +558,7 @@ public void documentInteractionOrderWithoutFallback() { try { client .createSignature() - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") .withSignableHash(hashToSign) .withCertificateLevel("QUALIFIED") .withAllowedInteractionsOrder(Collections.singletonList( diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java index 708eb380..e85c8df8 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java @@ -86,14 +86,14 @@ public void getCertificate_bySemanticsIdentifier() throws CertificateEncodingExc .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LT, "50609019996")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LT, "30303039914")) .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .withNonce("012345678901234567890123456789") .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-50609019996-MOCK-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039914-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIzzCCBregAwIBAgIQZ5j2PEu1zGFm2sQyec4uhTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1ODI2WhcNMjQxMTA1MDk1ODI2WjB1MQswCQYDVQQGEwJMVDEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGjAYBgNVBAUTEVBOT0xULTUwNjA5MDE5OTk2MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAhETp4Pf67Q+DMpJeXqF+HqAMMDMbLcnGCZW5z457FYIqg924MFH/QkRv6JQcZnG0X6QbhRZrHpAOCUdkLqpwr1fkv59P4tGS+gqXGl/CPqHUDjg/ue8H0NQdBI9E7YC/jdT3y+1vudL5GiRgfaUyVPLZrABLfQ9IKNrw83blLiPJscEmBckDKAqQej5J7G6qPZL2gDMEKMFx/uMsvUYlAXL7HSsHHr+Et/uQWezJzTAR6uf7MCseFmEF1pDKKdK4ZA1W70ygzVQxgl2BI61Qmwbrz6o9eowFui8x5YebiGpW75zQk+3LHcOi53Y3YA9mfmjMKjWi81JOxPi/wEUXRJxotZ3vun3A3J45K8D0BD10AjdyFPDs8YWhgfgTWnliGDJDrnG5pD2LNr0XKwYLbqHnDlAbhAxGC/M3RPZROLOtA4y5NRHIZd4URTDts2lWEu7CfxiAvbqUXUAE4SfYtKm9KQeWf1KE20Rz7wBUUgNio2xiGr/phgjOzDi8QIcsw/4DiomfVvU+3i851+2YPO+JSs0wUazY6vBjHAC80ti2T1U21ctU5Ch7ITkwX+/vie/HVVq/gGEkkIkoKFf/CXPipSC9Q+/BoFYmCWQhqHynnu7vtXKa8mRwnLKHoJThgo8s1vEQ6w9aNWlzSNUmKlLh2YKyq+0OG9+ZmWRjy09+rsITi0RtQKpHtTKPKa5S08pI+I269rC58lmiEpo0nlP/0q0mmrv9t3sIi6UtHpoGXB204FoNMWXTvfbbbo5J4pSEufkTf132R4HB2rqPVtrm/I2zLgEPCdxruFOWteedkqQb6vNdRYhmg6dJfdByqj8XanyrH9zO39L9SXEQfqp7x+TVf0kEx/0x1q9E71dFNyIMgxt/avdA/S9oPi7ZAeOiWGdSUsSJgVuZzLLfXzwTvJKGy3WN6W+efTNw2tVdk8N9VAtseZE8sGs/FGlS5p8kiWKN56j6ZiehXcuM6pbWYumkuel8fq+ucV5KAahq3Ym8OTEcl9mZJ8bYTs7fAgMBAAGjggJmMIICYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFGa4Jvp40NbeOGbzE+cDCuJfATqiMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC01MDYwOTAxOTk5Ni1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8yMDA2MDkwMTEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBADNS7S82pT9RC1NCfMetNc9wwCkV83SUFlyTrOfr/c8jhkhQKfWCYegwEsUs3K+rUznE51JtLWoCYC0ftkdoIcbdXsR5d6Lq0jEcPwCG0JfSWuS8QFIKDZ/2FXNSjKAYbtqRuKDX+C3fhUjC9OgChGAxvnbXyvzY4whhmwCyQ6y5BpscZXE8+kSndfJsOJZV2Agd9t9WOVNg61DS9NJJ0OkRHn91NOQY3SFn5rpqdd7oOnd5ooUqmBPlcu0VByXXffUI+hJbiLdM8YhdbfHrkdqa9DfhgbOEEsY1YDqFVKgELCL7ISJ+Bsud+dy3VplVxiTQd4xQSfgEnPayhv/f0c9I7kYSnAe9fhz+sMqIgg+XEvms671QwCr5hJVCUUV+biFcYAemKxBWKzmPHA/1x5c337qpgEBnT51mkk+tz7jGG6KxmXLFVhJHPVQ+lKoEiZuMwvcAW5IBjZs7+aJe7E156cIr30T0LCnf7VK6++vZ5juePJ8i854bJZ27Rpds0TB2+4CWLwqk18hNHD9uVY4y9huruN8ndUNxHQ6cPiUsXr4c8f6Yh7gagJGcsfYWBDMVsXG1Oo6K0D1v5TEaESXSj8/3pWKC5Wj2tVMXNEcTESlEZ+JiXnqPQplOQ/MrI+ctsLgha+XSlWzyYDnLWikZOfQ40AvyxUZMfYvtwtIu")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIeDCCBmCgAwIBAgIQcnLdjYj7nH5m/WBe9hNIpjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTUwMTUwWhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkxUMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk5MTQwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCYPFgri+lor5RVPUHuUHbLiHZFJ82WijgayMc1Bnj/fKQxOlq5DWX73Tozuebbw96+1t9qTX3zek2uYt+PZ6pedo0ZF5JNmti+zTgBqF+/KvLoUB9Kas51NYugKfRJDx38GXXRG/rpWI6PiumrDEaoLLi7eMfShZT49Bl5CxeZbTWhMttt/TJQ2KTJG4rVLXam8N8cXm3oQt1SA1e7Ceiz1Xx9y45HEbQovufYB8/YQDnp+wDzFb1lN1A6K/RBmSxKrqXXNjxkFHgaBkZ1YzdWM6NcvB9cFsSCU4w9FBLkcvpYprc09TuFok4xNnxn86hjdMEZBUQhE10CODGHzmSKD+KFHULSx+b0FccGjMaFQ0/79rau2+YjOGHF+yoC9bAg6XtmPZk68ZBK2AHm0bC1zzYsyzbqWh+gLq41fGzZBFvbxzaXwW42oShf8+47fV0zKZfqBC9q8Arg32wLJY0kr0k/lkGtAO+rVuok3wwH0ncddKP+OHbR2IgTicsz2xgnF+8ItqRSgJ5yoNuMWUzNd7NBGTActryA5cydHfAZv/61702jEqz4CdaNPWu6evvv18wFkys0M9BKjFjPcHbXDxXp5N3/XbGRHyu88p/dNWebx4HoDX5LepifQYxJ9OjTTP1BJAv1LdnFyN4juzEPxyO+CbIB5oqsuxUqhUXR5AB1F9vkLGANuejPSqyKLV8qVYbBGQK2sefqUw9LwUFrPh4sV+Pz4uI3bA0uDA2r42MFkExMk/XV57yINSdJUG1E75NrFIMNbuMUZp2cbmKIuIrupXn1tGRGQhsjlpGkdBvonGHQZPdzlIrOG4qFEJB72uBHqeIlJpfeBkdSC7f9BqDB5mkKuVZ8Fj55lWCU2xkzJ2UxBeyCfRElFSBCwo/AlAxI2fGCZkjl5JIZ0rslG6rBm21cDuaLfspYifizzFJ0mGsJ+iqtU/eh2KxRcbRKRj1GMkOWS3E1tiriohvjoxQG5xF+u8s/ht5TP62YQfG5Dkl++T7wOEnnGMGsr5UCAwEAAaOCAh8wggIbMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFPem4JAsN+0DrjswFiQ8ZYejemZcMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC0zMDMwMzAzOTkxNC1NT0NLLVEwDQYJKoZIhvcNAQELBQADggIBAJ8yCBedb80PyP8YOFUZ4g+CkZNtXjWJ+2F6+2p9qfotHxYbJCQ36PSuq8nD+9+VNSXKLINStxHSasCmDoX62/IRf38tXCXHBba9h3gi2Cw5Q5oINV7WaMLQohU5MU88udNDYWvVcho7wEOkJ0EkXR4pEnOhtrol8hwAbNU2iP8jAuq3YocwyayEzMBm7CE9T2hMAf3H2TzydM7dMLmwu5/HDX/GjqpKBMXNeJPhW3L9FVJVdGhkBKiSyaXAqui46t32OkYO2useovah+yNX43Xvc4/ESBeA07pgJH7ATO0KyFcfV5CRVgq1WUm1NL69wP7OAEX/T1QhCiAJcJaxIzIGsgFmqbFLP9Q0+KaFSdFW0ZEWkDNmaThXXVm7dGY9FP90DOvqgr36thT9wrZBdZid+fsljBa7gxc92GUiGJ9f1t0F2uHJRNYzMdldApr1uh6hwH/VNy3U7uKdT7VLmJikK6GAHEbUR9ZQIfKBvllN7nyhfK90HUnAB0FfdG4RYyCaZGeKi7mJxGxeJGzkQB/GnWHTmcKasKHWJKolXFV/HdQt2sI7VUDdRgFs3JwADeBWnRCEv/DCaStvHndcsxzzV7ZjvVyC3COjx/jeldfBqiywgGQu0bPOqJJ0p5aYtDjly5cOEpGWKhVO04O6B2DvxfxyfKOg2s/FOcHnf9Gq")); } @Test @@ -102,14 +102,14 @@ public void getCertificate_bySemanticsIdentifier_latvian() throws CertificateEnc .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990")) + .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012")) .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .withNonce("012345678901234567890123456789") .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-010906-29990-MOCK-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-030303-10012-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIII0DCCBrigAwIBAgIQPnChot3bJ/pm2sRs94tubjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1OTI0WhcNMjQxMTA1MDk1OTI0WjB2MQswCQYDVQQGEwJMVjEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGzAZBgNVBAUTElBOT0xWLTAxMDkwNi0yOTk5MDCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAaFa/V82Gld+21Smxj/CB2etFLNx4QtWyWtNNlErvVysSmwL9jwfBJKQB0IXWo4GhQh1eucQ0gUBpatGVoghf0BB4AZJKamUV46RkaS63phR1EqHlaEp4R11hEs9ll0i9+Km5SINdBU9jqphGtyWJ/1iza9XEjWUJXS71slENdSlQjQ93LggYqxPtSXZ56KSfNWo5Uz8YBNo2aRhnp6h48HQrYk5WuoFW/uUm+Cf+4bO0iZzXGLIHZUTZKFJ2johcJdmfuwtApwzIS92U60gmQxPDG9yJrix6XHhfxYf1JTc4uMmYmTWvkUvV4qhUKU5F1jak1+3Le/+n9IVWN43qNTEFQJ8xINlfHsnlJ+5PNNR1HLLTmv2VQ3IFRB+EWFY6+w6OQgfHxyNDZnx/qqfvyVBllQ65p14VHsMv7uS9htBTkaDIT6995QVVdtFfORznNWKUVz/uPG2KRsQQxrFaOYCtJJ7bX2POMUquaMTv197pi1OcuHVyO4OZl9k/owVJeg0UKq/LCSaxT0F4zBr2twss3YX+5SgG2TyYANn4wU34rDzuPv52/yY0FH1IqWo2eglNsZSzxDhTjpQH3Gi4ogY7PkSg+yd1J3j9vSpBVsFV9FjRZ4xEV5CXMZCKHTWHP8aMIkpz1T6kY6gTV3aIEccFRvGWRzp6BQCRYGVwmtVv3UiLXqYiSHPgWyZwbyVQQu0nOPotNMpBo8XdNgYUneOVApa/zJokPXK8L1SiBlYEJvBfDwajS9VWvvpDznzqoo64np1D2YDcF6Bs7uZpg07oRbj62pXI1oG63weAgGw0cp266Mvkv2eIXgUDgh2u4LzGG71X0Nd/BJBeARFO7IWI1X63UT9hNOb8epRnCD3r69DSXejRnp4mdb35bNgDM5+V8acijVa55nq46cAk7BvMdkBW9DsJfoQ1msPeEf1lRuhulXyOHwg5BSAxc26+HTTj8rTKXT8lNufxsd2RkamlzLVVGm1+DnBBCTPf4JzjqE0+QMXuql0UEaCRJA9PAgMBAAGjggJnMIICYzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFKlFcvS8Z6fL+4bcIKckvaEM1ldqMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMTA5MDYtMjk5OTAtTU9DSy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwNjA5MDExMjAwMDBaMA0GCSqGSIb3DQEBCwUAA4ICAQDXIkSD7h52Qu5bPj1D/iY91Jlw6EVfhEnVuq4guT6J3jzE4fcYDNehI6P6Yted7HKCZEXW6kvvqxWx76GV7JtfrBZzu+Ru2gud1+wwVfgAkfyPkquklop4/flpDBz3bQCVkAbmdNLa+x939tGzlPIyL68JDEvHtxDLUa1mrAY8c2TNxcBAcUSukzE9vBfvjiuDoCsRylZ0DuEnG7qQ7qn+LGDFtWBiZ120V8ZLQpNRUkhkthYwm9aAt6j3l/KzawOB59rj7eJ1CUx7yfdpmB1M4BwAI1JOh0PMcpQ/gUnKB6CbU6SjTnexBTIGllht6WyZhyfKTs82useOorwn6PREVwhftIqkO/LYZD2dgzyqlNsEqglYU6oMUYKf4SbVhUZtYBuq9wTvvgsIGj9XYr8/9ZktnsWOWT+CEbmsdGncyJo1ubLSF4f5/NllZwSUdNqboM3rquW/IlJEJrjrOiepEdMYEEoz+zL93/RzZe0xGWitteYlfe8jXJilh9cJHGH6I+CM/s/lkgorRdKP80MotSRqBsztaeNLHq6r6Bls9P0G1PnEIzMwAwExJ3pe4NWjTfJud8coMXgeUhtxr9zqUr+hpXg5WHGCHxJ0qoR9x/YUk8E6szG5ccykk2Eu9tmPVoAaSAPPolQ10c2dLMvPtK8bDJr1dB8ht5E1zjVUqw==")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIejCCBmKgAwIBAgIQSMxs7XxO4eBm/WFlTLHbUDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTUwNjEzWhgPMjAzMDEyMTcyMzU5NTlaMGQxCzAJBgNVBAYTAkxWMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEbMBkGA1UEBRMSUE5PTFYtMDMwMzAzLTEwMDEyMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAminatsgmv6e/jO+OQy1upDQq6MfolQ35cJVSlPnDHH/AOYgG8Qd96lqxYWbYsTx0A3anP6H9t7ctO+Ur6CrO08SxQm2NwBudCYcoseaBd0+KzHPKAs5rTzwQenrsMYVVdwrvE0scB6t50bIHBwyHYxevJSOuyQ1qUrIQJu/jzrNTnKrmZuBTi2310otePc/pYPy8UsqG9EUHiRdlsK6HnfdoHJa8BgJgIPbTOPWS0PGy8Q3cZxw3EjHmQLZmtAY+QK78PwYIS1I9xrP+RUI9o2piyvt/D1EuEwwBXRLrvdEtrWCOdYnSyhT0+8tWiKg2AIBtb8zOOEQ2df4VUqCFcXa/Pr9a3d+Xx3Ikj2tVxjfPteJ1RO89/CMi65klT01y5nGn0mTpW6T0YMSbQFI2bckVnem0VpucK7qsAGqSnI9QYGNGbn50Y0J5ZF+sbKJh5GLnr65vwyhfgzKlgHx3Ag/fMIT1gXdvLQyIFlj9clt8H93aaCV4lHPs5jmRaXxVkQbshaUCNFTFNAZY9kM1OHlbtmnem9aZCrEUfaLbJ1Fh1mCbVZLmZs3qPDErW29MqXy0qpiFkLKiF7EaieuiSsVcWkel/BCzsgqWoR+vX0RV5ydhoM1rNaOssXKO1zu6YQyluElOv3zIomStU6s9Hy7x3i+6Qi1u32fKc7oK45uzagh642xu3ziY3BFeAoJAHooVFpGxo13a3wekzwsmJfA/CJfVnAZUCwP1QmO4R0XWOb8UBDMQqGpsyroSbn+QqutGb9xCNx8cN0qHBUkzrwz1eWfWADx8cdLlfn06OorGP0ORyKGPRtPk03lfrDwzZoFky7lxoXIUqVj1xfB9R5aVtS/e61gWu38hNbe7TKOcHz2/SL6stctEUvcDtmRoSwj+kN8DbFUOg7xPvmwVmuJqIVtPQFDPKFmkjwzaOoilI3oLwcJwCXGHTWVTvnH25bQSLEpR6urZnQMipHXLGGyXMXUtwT06oajWIGq4MMY19QM43b24c1pIgGR9WKjHAgMBAAGjggIgMIICHDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTjDtjZcvT0VXcGijoC8vVFuptWJTCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMDMwMzAzLTEwMDEyLU1PQ0stUTANBgkqhkiG9w0BAQsFAAOCAgEAnosMKGxxp7mNkvFwCrYcn4anl2QBGVyLuQ5IgE/IgER5/H4XKXmh7mSl+XzgODx2LYSd9ZqQLtYnDek16m1A0e5wBYyxjditQaG481/qOl+mOPSP8cmpEyYaImve3jHU0jZbdyehGWX2DGdda67asng6qADLvTWOCHFyIkCErLXOLEIR+0NqhOQqBrJKAa4Iy/+HhhFexkvQn6IWbduW3sUQdmyHIpXuOyXRS17+AUdpwISWF99DeZCwuJw/yGdygbRCu3J2UmzT/7pOtETJsw/2KgYFl0PF7YLar/rTPxwSc0X00tDp5Bx8Uzh97c6Ytsy+joVQdjWzhXUa0tYkQKKlfCshItwl5OXnidTf9axJdt4eINk6hSLWjlJ9+AvSVsTa7fH3CrIzz+ueADKgHY3sDvtfv1S3E3rGQSt2tss3gIxWGv0jAmq7a+XCfK4G4ypKRf1Fh6rWloDw8GjDcqlStnLLXt9ytAe4hoaZEna1g1KpMJZvzJGX2ejb/wzN2PFNgF3sqdSTlwhox0AMjBW2bjRHa92iq7qCXV6r+Od6KQVOAPcKv/aPNml1sdqGwx/w1qsj1u/amHe4XfPhQxQX81GFqm0yBgrNwmaZIoM2S0dUNA/Vu9/oQ4PmEzTvCxBtLdLD5nsJTUWnl0Guj19Z7L3A+7TLpHk3XLIgIRc=")); } @Test @@ -178,13 +178,13 @@ public void getCertificateEE_byDocumentNumber() throws CertificateEncodingExcept .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOEE-50609019996-MOCK-Q") + .withDocumentNumber("PNOEE-30303039914-MOCK-Q") .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .fetch(); - assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-50609019996-MOCK-Q")); + assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-30303039914-MOCK-Q")); assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIzzCCBregAwIBAgIQeQ1J2QDJ3Jlm2sN+JMQbPTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjQwOTA2MDg1NTI1WhcNMjQxMTA1MDk1NTI1WjB1MQswCQYDVQQGEwJFRTEfMB0GA1UEAwwWVEVTVE5VTUJFUixORVcgUFJPRklMRTETMBEGA1UEBAwKVEVTVE5VTUJFUjEUMBIGA1UEKgwLTkVXIFBST0ZJTEUxGjAYBgNVBAUTEVBOT0VFLTUwNjA5MDE5OTk2MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEArMoqLywQl6M6o1LDFW4iC2BUkyZAC+jINmWSQ0rispoTEOslzGw/aTfao8Yn5/KHZWKqZiFC7sy5qTGFOKt8xlTB7HJvKE58XsteZi7lTkxpK0m3haZQeb3G6dROKmfwtd7CrvSz0CPWaUogkPUZoO96fPuSs/xcWb4lL/M7OK/t8tdSJ5h/devSmVbGuL+Sder3FuyvtEtT8R5JnjChbEp6d1B7bIfKpgw8bdTGPbQvv145t3eQnCo3lx8vcZjTgAkiFoH1HmwrxonLnA40+qUKztxrbQFTi32dvxKiUPatDeCAKHgl4OXDZaOUvHeBylbd8I6aQ5PFHsXgBd9jccAHwaXYDM4qvSggwrHJNDujPa+drpYHL0f6N8pd36MrGiSekPyDTcg9RuIetUD+UzLO3vFusbC/anBaWs7UYaK1iTNT9AqlEhcnrovWZIuZ7/f3KSrnBkvJ9IQmdDbvwWg6m3oj6EZR9rxa8B+x9YWEYitPXbgwsYj5lPxyzGDYCtuXg42Xs0YbVwyWTfJu3Jmm113xjHbKQYdZrgxhFldqjo2W8FdFiggi3VaPUQa39GbC6/nSj5VKkglbTiH6JwP7edQaJ+5VikT1lAXJUHUQ3XYFGC50lhUrVrcYjIUMODBqux84i445ypUYZ83HAnmxMvsgjSVGAWsfIRYAnjC0nuVjhQRhNbX99LJ/aTu10fx9wfjOwxeSn6WLwWxR3I106thh81EtmY+qwf7Irb70VkswSGCd9k/UwXbv8LPdO4NgvNmuayaN01P3BGzxTP4W2mr0ATp3z0dhT5vz1jUxvHpijErlJwpVv6aOEAY68LnUvfy8jpOpiWvhiJOSpEE0yhVIx+/qfV9c0i0nw/ermVYupTOn44XlvqmePf6G/YwASV6vpi5aGtFeDvCPmiyxWrue/UWmJPE5vdueOWmdVokst7RVCUSXFEOC13O/6XhyofdPk201gSfhEwdsB/VKREXGVCAfjs6bu8IF7KEiOE/eSYNfb6nHW0BJNIqHAgMBAAGjggJmMIICYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDB6BgNVHSAEczBxMGQGCisGAQQBzh8DEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFF3jcJ30p6biEtl7V0Inb2c6XjZrMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS01MDYwOTAxOTk5Ni1NT0NLLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8yMDA2MDkwMTEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBAF8gPJ2SkrXxGjqOO536D81KPqAxWbHdcVCzzg5LIRgumGlbbb3OyGSpmRfO+lHNKsni9XJP4kVwn6/9w2rRBmEm/x8U0ZoelWD6SNTPWggb787B3bAxZtEOEBiEfiJc2iCj2ZGuaLrzcz/sjTYo/+11X9411YBvDgHOYferCV8ms1IUv1mhWeE5jEn3jjxz8h04W4A3fN/ydOdTryxBdOV7+giQNQe71tOx1GQpDg6IXA4Da6CPUJxadGcWylAnOQFV1lkKk67revwkF1Z/2yAnTDz+3bx3DjUIlZXKx9Qg8/AXkcu8+ONvQaDn+QLp1qlRtcTUDfFi0bHiKPpv0dMvvGfEPug2G5QbA0jiWwmaZGJfdxBaRirFVLVl4WEE1+Sp5J8cIqEBpfCeLVDcpB6z2T2PAywL932QSHQ3jd/gwuKyZ/4VYxplnL2LazNvEh/Cv8JcvHoxh14bRRWWikdHcgB6K1TJ1nvnQPWnOBVPHp+W+1JYh26eE50dOW7UmqUrNgBm2FVMg0c6nufLghIwqRSHvJ/bX0Ovqby4aKy0Es1sRJNkYcuRUNf6LCMS7uR3EO4zOoiAzpUA5IEM6UUXMG92qNaJNT1uY/ImuSafSuRTd82SiBvl4XazNSl5Hgo4qMwD1SNjw4AmoFFi5dns7LYIqitnhjcUOtlgazE2")); + assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIvjCCBqagAwIBAgIQTyHvSQrAdk9mgogev6nM4DANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQwNzAxMTA0MjM4WhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkVFMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PRUUtMzAzMDMwMzk5MTQwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAGlRrnunxGGVyekLA/sTMgCNAZ4KX7vR1BtK7RlAfsfxK+7dWevL1yuTkSs67xIgYUhaRQhck3f6Px2izcDzWHZy7Z7h8CzuZBR8YjQzQmfzIxurInXCtP0vmU4lUMfHg0w2OMHoSYOCmPEh5H+57BCom/zwHxciW92pRZeovyyj1qXEdtdHbDks2peKScNz+rcImv/53eTO2wPkjno5yzTfsbammlBLG1IYaA5VRh3YGFwfRVBMdRoV92Ym7x7vDh9LiZXfoK6+lF5OQ+wwBVjEYVqAlVaTxmtmXiY4mcekTZ+cr5mULOJ7QnD7+NRSSjBGLwEPVTweNWnX8D6bJq2VeZWoKahPoqwbv6g9dda5Hu+6fX36ed88h5A8VT58743BqITQ4fA10EpF5uC6a2+9V1BA1UQMUtlBGGZweof9bW3nE7EWSR5f3U+koW/pBrGFUsnpBSCXWP3NqWzSggrYOqY2ErrBoE8FxEKsHU140X+8SWtxxLc0UyYGn21hEBk66YbvwqeSApNRh3x/aQMyJtNCoNEEGxc8vgAHuiQePUPLbTeFl+cfyvrWANTzssMqJStfH5LunAzLyh3qgZ+qe8qwUUl9PMIKsM8fIYMHQUAFZddKzLrQz/iiDvKYwmG2iRZx7GgM8ubN1LKP+ld9+YTXQ/aUJnQQjjgCe/ySuiq8U1rDe77J/hB6S+UD268hGYCSo1FXkda/ekVbvnCvl5CGmHUp9NRcmjfthvWffdy+CK1F7ctPBHDVbak3/SZcJkcQaNerKmuTWOdGjpw+pZkqrDdVaDWGmb8XtRccGxeir99CaNqJfN4CTov/CqV899Pk70LUYw/+8grViREozX8chofFEBUAeIRzJgyAdIteDlj9lzCEFlqXDN8MjfepSDVpF0po7BAs7tyrUoJ+BjIRdKBS3kH87iMZhUtxtevhhP/P9xV0lLqlPuRWjr5if7kL9U3WPz/ws/5O517IiBSktvgInCZ4I/+gSka7wJvsuDD3fMSqfGERTOtrTwIDAQABo4ICZjCCAmIwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwegYDVR0gBHMwcTBkBgorBgEEAc4fAxECMFYwVAYIKwYBBQUHAgEWSGh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NlcnRpZmljYXRpb24tcHJhY3RpY2Utc3RhdGVtZW50LzAJBgcEAIvsQAECMB0GA1UdDgQWBBRjqdXLjzqj0aJobR82+uqO5xYhHjCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAwBgNVHREEKTAnpCUwIzEhMB8GA1UEAwwYUE5PRUUtMzAzMDMwMzk5MTQtTU9DSy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwMzAzMDMxMjAwMDBaMA0GCSqGSIb3DQEBCwUAA4ICAQDqZEspnUbRmbHUzWVbeSaoVgDqZmRNGu8BP+p/fP5T8NvVRUbinVZ3dq+6/+hhUxyKUDppNYzWYoNa4Zdamf16z4yvPVwy/z3D3EthNX4ePJQSHVk6zQKjBk9/4Hu4u+9EEsxJW/z1F7DHkfL94/KCG8fOybIK9uwfMQLFVgH4gvT4CWMXlxkw7R6PI2rnfrCGtjzLo536aJ23i4smns/wL83G+g92jy/zXD3PyuOlHgEWXaWoLdG+pKdRVTus4ppkbfLsaGTvSEVLvcwHhIWVOi0TkeNDjQpKe92yNHEIFc1SRq4I4h9bbgAWZRDy77R3UDmRrDZGlg2WBZ+tdmo0saqi0fdhihAx5PzupltNLbamJu3yz/HBZvbUw+Dz2ADfCTNT9cf+o1iaeFtpPy/iPSepZtAWmQXfmtPezgFliFMSs0q5tGVRcai2+PrRrvrzIT6+5aeSsfCkk1SlgfLeYp1P6Fc2yI5MoMux2y01sFhJhpuhDf3nh0IE4JJS6awulr6GXw41ipCopCWt4oJsfXCFJLZdhxwpMtlrqZ21qY7dGbV85X012o+78CFGpbRh5wBC0cwi1hDhq/aM6FZZHY0Y+S3NVEDnbb6oz2muLksBvXZiz+8iSWHT2bReUYMRTnM3XXtEjUvhHYqIJiMqgAqbAJ2ttRtVrHj2He4p7Q==")); } @@ -194,7 +194,7 @@ public void getCertificateAndSignHash_withValidRelayingPartyAndUser_successfulCe .getCertificate() .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOLT-50609019996-MOCK-Q") + .withDocumentNumber("PNOLT-30303039914-MOCK-Q") .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) .fetch(); diff --git a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java index 34c15b29..d556c11f 100644 --- a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java @@ -55,7 +55,7 @@ public class EndpointSslVerificationIntegrationTest { private static final String LIVE_HOST_URL = "https://rp-api.smart-id.com/v1"; private static final String DEMO_RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; private static final String DEMO_RELYING_PARTY_NAME = "DEMO"; - private static final String DEMO_DOCUMENT_NUMBER = "PNOLT-50609019996-MOCK-Q"; + private static final String DEMO_DOCUMENT_NUMBER = "PNOLT-30303039914-MOCK-Q"; private static final String LIVE_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_live_sk_ee.pem"); private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index 28944673..9a66ee76 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -61,8 +61,8 @@ public class SmartIdRestIntegrationTest { private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String DOCUMENT_NUMBER = "PNOEE-50609019996-MOCK-Q"; - private static final String DOCUMENT_NUMBER_LT = "PNOLT-50609019996-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER_LT = "PNOLT-30303039914-MOCK-Q"; private static final String DATA_TO_SIGN = "Hello World!"; private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; @@ -88,7 +88,7 @@ public void getCertificateAndSignHash() throws Exception { @Test public void authenticate_withSemanticsIdentifier() throws Exception { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); AuthenticationSessionRequest request = createAuthenticationSessionRequest(); AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, request); diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java new file mode 100644 index 00000000..81bc2b78 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -0,0 +1,472 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +class DynamicLinkSignatureSessionRequestBuilderTest { + + private SmartIdConnector connector; + private DynamicLinkSignatureSessionRequestBuilder builder; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + + builder = new DynamicLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withCertificateChoiceMade(false); + } + + @Test + void initSignatureSession_withSemanticsIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + builder.withSemanticsIdentifier(semanticsIdentifier); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.getSessionID()); + assertEquals("test-session-token", signature.getSessionToken()); + assertEquals("test-session-secret", signature.getSessionSecret()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + } + + @Test + void initSignatureSession_withDocumentNumber() { + String documentNumber = "PNOEE-31111111111"; + builder.withDocumentNumber(documentNumber); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.getSessionID()); + assertEquals("test-session-token", signature.getSessionToken()); + assertEquals("test-session-secret", signature.getSessionSecret()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(documentNumber)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { + builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.getNonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + } + + @Test + void initSignatureSession_withRequestProperties() { + builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.getRequestProperties()); + assertTrue(capturedRequest.getRequestProperties().getShareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @Test + void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(HashType.SHA256); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void initSignatureSession_withSignableHash(HashType hashType) { + var signableHash = new SignableHash(); + signableHash.setHash("Test hash".getBytes()); + signableHash.setHashType(hashType); + builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities(Set capabilities, Set expectedCapabilities) { + builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(hashType); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA256WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @Test + void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(HashType.SHA512); + builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + } + + @Nested + class ErrorCases { + + @Test + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { + builder.withDocumentNumber(null).withSemanticsIdentifier(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Either documentNumber or semanticsIdentifier must be set. Anonymous signing is not allowed.", ex.getMessage()); + } + + @Test + void initSignatureSession_whenHashTypeIsNull_throwsException() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(null); + builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("HashType must be set for signableData.", ex.getMessage()); + } + + @Test + void initSignatureSession_whenCertificateChoiceMade() { + builder.withCertificateChoiceMade(true); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Certificate choice was made before using this method. Cannot proceed with signature request.", ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlgorithm() { + builder.withSignableHash(null).withSignableData(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { + builder.withAllowedInteractionsOrder(allowedInteractionsOrder); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Allowed interactions order must be set and contain at least one interaction.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { + builder.withRelyingPartyUUID(relyingPartyUUID); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Relying Party UUID must be set.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { + builder.withRelyingPartyName(relyingPartyName); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Relying Party Name must be set.", ex.getMessage()); + } + + @Test + void initSignatureSession_invalidNonce() { + builder.withNonce("1234567890123456789012345678901"); + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + } + + @Test + void initSignatureSession_emptyNonce() { + builder.withNonce(""); + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableHashNotFilled() { + var signableHash = new SignableHash(); + builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(UnsupportedInteractionArgumentsProvider.class) + void initSignatureSession_withNotSupportedInteractionType(Interaction interaction, String expectedErrorMessage) { + builder.withAllowedInteractionsOrder(List.of(interaction)); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals(expectedErrorMessage, ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionID(String sessionID) { + var response = new DynamicLinkSignatureSessionResponse(); + response.setSessionID(sessionID); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("Session ID is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionToken(String sessionToken) { + var response = new DynamicLinkSignatureSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken(sessionToken); + response.setSessionSecret("test-session-secret"); + + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("Session token is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionSecret(String sessionSecret) { + var response = new DynamicLinkSignatureSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret(sessionSecret); + + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("Session secret is missing from the response", ex.getMessage()); + } + } + + private DynamicLinkSignatureSessionResponse mockSignatureSessionResponse() { + var response = new DynamicLinkSignatureSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + return response; + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Set.of("QUALIFIED", "ADVANCED"), Set.of("QUALIFIED", "ADVANCED")), + Arguments.of(Set.of("QUALIFIED"), Set.of("QUALIFIED")), + Arguments.of(Set.of(), Set.of()) + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class UnsupportedInteractionArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Interaction.verificationCodeChoice("Please verify the code"), + "AllowedInteractionsOrder contains not supported interaction VERIFICATION_CODE_CHOICE"), + Arguments.of(Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm and verify the code"), + "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 6ad09a7a..122c4b1c 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; @@ -122,4 +123,48 @@ void createDynamicLinkAuthentication_withSemanticsIdentifier() { assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } + + @Test + void createDynamicLinkSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 3da46e48..388e26d9 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.exception.SessionNotFoundException; @@ -57,14 +58,18 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.SignatureProtocolParameters; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SignatureAlgorithmParameters; class SmartIdRestConnectorTest { @@ -199,6 +204,114 @@ private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { } } + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + + assertNotNull(response); + } + + @Test + void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + + @Test + void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + + assertNotNull(response); + } + + @Test + void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + + @Test + void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18081) + class AnonymousDynamicLinkAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18081"); + } + + @Test + void initAnonymousDynamicLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + + assertNotNull(response); + } + + @Test + void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + }); + } + + @Test + void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + }); + } + } + @Nested @WireMockTest(httpPort = 18089) class CertificateChoiceTests { @@ -311,110 +424,131 @@ void getCertificate_throwsServerMaintenanceException() { } @Nested - @WireMockTest(httpPort = 18081) - class AnonymousDynamicLinkAuthentication { + @WireMockTest(httpPort = 18089) + class SignatureTests { - private SmartIdRestConnector connector; + private SmartIdConnector connector; @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18081"); + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); } @Test - void initAnonymousDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + void initDynamicLinkSignature_withSemanticsIdentifier_successful() { + stubPostRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); + + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); assertNotNull(response); + assertEquals("test-session-id", response.getSessionID()); + assertEquals("test-session-token", response.getSessionToken()); + assertEquals("test-session-secret", response.getSessionSecret()); } @Test - void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); - }); - } + void initDynamicLinkSignature_withDocumentNumber_successful() { + stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); - @Test - void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); - }); + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + String documentNumber = "PNOEE-31111111111"; + + DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); + + assertNotNull(response); + assertEquals("test-session-id", response.getSessionID()); + assertEquals("test-session-token", response.getSessionToken()); + assertEquals("test-session-secret", response.getSessionSecret()); } - } - @Nested - @WireMockTest(httpPort = 18082) - class SemanticsIdentifierDynamicLinkAuthentication { + @Test + void initDynamicLinkSignature_userAccountNotFound() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 404); - private SmartIdRestConnector connector; + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18082"); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + void initDynamicLinkSignature_relyingPartyNoPermission() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 403); - assertNotNull(response); + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); - }); + void initDynamicLinkSignature_invalidRequest() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 400); + + DynamicLinkSignatureSessionRequest request = new DynamicLinkSignatureSessionRequest(); + request.setRelyingPartyUUID(""); + request.setRelyingPartyName(""); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); - }); + void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 401); + + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/dynamic-link/etsi/PNOEE-31111111111", exception.getMessage()); } - } - @Nested - @WireMockTest(httpPort = 18083) - class DocumentNumberDynamicLinkAuthentication { + @Test + void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 471); - private SmartIdRestConnector connector; + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18083"); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 472); - assertNotNull(response); + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); - }); + void initDynamicLinkSignature_throwsSmartIdClientException() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 480); + + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @Test - void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); - }); + void initDynamicLinkSignature_throwsServerMaintenanceException() { + stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 580); + + DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + assertThrows(ServerMaintenanceException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } } @@ -423,7 +557,7 @@ private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessi dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); - var signatureProtocolParameters = new SignatureProtocolParameters(); + var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); @@ -441,4 +575,20 @@ private CertificateRequest createCertificateRequest() { request.setCertificateLevel("ADVANCED"); return request; } + + private DynamicLinkSignatureSessionRequest createSignatureSessionRequest() { + var request = new DynamicLinkSignatureSessionRequest(); + request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); + request.setRelyingPartyName("BANK123"); + + var protocolParameters = new RawDigestSignatureProtocolParameters(); + protocolParameters.setDigest("base64-encoded-digest"); + protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + + request.setSignatureProtocolParameters(protocolParameters); + + request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign the document"))); + + return request; + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java index dc8a9bd0..2fd6b8d1 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java @@ -33,11 +33,11 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.RandomChallenge; import ee.sk.smartid.v3.SignatureAlgorithm; -import ee.sk.smartid.v3.SignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -86,7 +86,7 @@ private static DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticati request.setCertificateLevel("QUALIFIED"); String randomChallenge = RandomChallenge.generate(); - var signatureParameters = new SignatureProtocolParameters(); + var signatureParameters = new AcspV1SignatureProtocolParameters(); signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); signatureParameters.setRandomChallenge(randomChallenge); request.setSignatureProtocolParameters(signatureParameters); diff --git a/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json b/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json new file mode 100644 index 00000000..85a818f0 --- /dev/null +++ b/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json @@ -0,0 +1,5 @@ +{ + "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546016", + "sessionToken": "session-token-value", + "sessionSecret": "session-secret-value" +} \ No newline at end of file diff --git a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json index b5429a87..bb38ec31 100644 --- a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json +++ b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json @@ -3,8 +3,8 @@ "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V1", "signatureProtocolParameters": { - "signatureAlgorithm": "sha512WithRSAEncryption", - "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" + "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "sha512WithRSAEncryption" }, "allowedInteractionsOrder": [ {"type": "displayTextAndPIN", "displayText60": "Log in?"} diff --git a/src/test/resources/v3/requests/dynamic-link-signature-request.json b/src/test/resources/v3/requests/dynamic-link-signature-request.json new file mode 100644 index 00000000..c62d3ede --- /dev/null +++ b/src/test/resources/v3/requests/dynamic-link-signature-request.json @@ -0,0 +1,18 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "sha512WithRSAEncryption" + }, + "allowedInteractionsOrder": [ + { + "type": "displayTextAndPIN", + "displayText60": "Sign document?" + } + ], + "requestProperties": { + "shareMdClientIpAddress": false + } +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/dynamic-link-signature-response.json b/src/test/resources/v3/responses/dynamic-link-signature-response.json new file mode 100644 index 00000000..64a15dc6 --- /dev/null +++ b/src/test/resources/v3/responses/dynamic-link-signature-response.json @@ -0,0 +1,5 @@ +{ + "sessionID": "test-session-id", + "sessionToken": "test-session-token", + "sessionSecret": "test-session-secret" +} \ No newline at end of file From cc9a8c70fed4e1eb6b02a25cbc12ae4f3bc3c002 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:23:23 +0200 Subject: [PATCH 08/57] SLIB-49 - Add notification based signature request handling (#93) * SLIB-53 - added dynamic link based signature request handling * SLIB-53 - refactored signatureProtocolParameters, fixed tests, * SLIB-53 - added ResponseParameters validation * SLIB-53 - fixed testnames * SLIB-53 - fixed tests, removed unnecessary code * SLIB-53 - refacored testklasses, fixed README * SLIB-49 - added notification based signature request handling * SLIB-49 - code review changes, removed unnecessary code * SLIB-53 - fixed README, tests and refactored default signatureAlgorithm handling * SLIB-49 - refactored default signatureAlgorithm handling, improved README * SLIB-49 - implemented unique device identifier check * Revert "SLIB-49 - implemented unique device identifier check" This reverts commit 3d2131287046cba633c394aab5be3f52cdb6983a. * SLIB-53 - fixed testname, removed unneseccary part from README --------- Co-authored-by: ragnar.haide --- README.md | 275 +++++++++ ...micLinkSignatureSessionRequestBuilder.java | 8 +- ...icationSignatureSessionRequestBuilder.java | 363 ++++++++++++ .../NotificationSignatureSessionResponse.java | 56 ++ ...uest.java => SignatureSessionRequest.java} | 2 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 9 + .../sk/smartid/v3/rest/SmartIdConnector.java | 29 +- .../smartid/v3/rest/SmartIdRestConnector.java | 42 +- .../smartid/v3/rest/dao/VerificationCode.java | 57 ++ ...inkSignatureSessionRequestBuilderTest.java | 67 +-- ...ionSignatureSessionRequestBuilderTest.java | 538 ++++++++++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 46 ++ .../v3/rest/SmartIdRestConnectorTest.java | 251 +++++++- ...otification-signature-session-request.json | 15 + ...tification-signature-session-response.json | 7 + 15 files changed, 1704 insertions(+), 61 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java rename src/main/java/ee/sk/smartid/v3/{DynamicLinkSignatureSessionRequest.java => SignatureSessionRequest.java} (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java create mode 100644 src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java create mode 100644 src/test/resources/v3/requests/notification-signature-session-request.json create mode 100644 src/test/resources/v3/responses/notification-signature-session-response.json diff --git a/README.md b/README.md index 329a6dc0..9af1fcea 100644 --- a/README.md +++ b/README.md @@ -1203,5 +1203,280 @@ builder.withCapabilities(Set.of("QUILIFIED", "ADVANCED")); builder.withCertificateLevel(CertificateLevel.QUALIFIED); ``` +# Initiating a Notification-Based Signature Session in API v3.0 + +The Smart-ID API v3.0 allows you to initiate a signature session using a notification-based flow. This method is useful when the user is already known or authenticated, and you want to initiate the signing process directly through a notification to the user's device, without the need for a dynamic link. + +## Differences Between Notification-Based and Dynamic Link Flows +* `Notification-Based flow` + * The user receives a notification on their Smart-ID app to complete the signing process. + * Suitable for scenarios where the user's identity or device is already known. + * Uses different interaction types compared to dynamic link flows. +* `Dynamic Link flow` + * Generates a dynamic link that the user must access to initiate the signing process. + * Useful when the user's identity or device is not known beforehand. + +## Request Parameters +The request parameters for the notification-based signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. + * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. +* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +## Example: Initiating a Notification-Based Signature Request +Below is an example of how to initiate a notification-based signature request using the Smart-ID Java client, using both the Semantics Identifier and Document Number endpoints. + +## Using Semantics Identifier +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("your-relying-party-uuid"); +client.setRelyingPartyName("your-relying-party-name"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Create the signable data +SignableData signableData = new SignableData("Data to sign".getBytes()); +signableData.setHashType(HashType.SHA256); + +// Create the Semantics Identifier +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "31111111111" +); + +// Build the notification signature request +NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withAllowedInteractionsOrder(List.of( + Interaction.verificationCodeChoice("Please sign the document") + )); + +// Initiate the notification signature session +NotificationSignatureSessionResponse signatureResponse = builder.initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.getSessionID(); +Vc verificationCode = signatureResponse.getVc(); + +System.out.println("Session ID: " + sessionID); +System.out.println("Verification Code Type: " + verificationCode.getType()); +System.out.println("Verification Code Value: " + verificationCode.getValue()); +``` + +## Using Document Number +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("your-relying-party-uuid"); +client.setRelyingPartyName("your-relying-party-name"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Create the signable data +SignableData signableData = new SignableData("Data to sign".getBytes()); +signableData.setHashType(HashType.SHA256); + +// Specify the document number +String documentNumber = "PNOEE-31111111111-MOCK-Q"; + +// Build the notification signature request +NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + Interaction.verificationCodeChoice("Please sign the document") + )); + +// Initiate the notification signature session +NotificationSignatureSessionResponse signatureResponse = builder.initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.getSessionID(); +Vc verificationCode = signatureResponse.getVc(); + +System.out.println("Session ID: " + sessionID); +System.out.println("Verification Code Type: " + verificationCode.getType()); +System.out.println("Verification Code Value: " + verificationCode.getValue()); +``` + +## Examples of Allowed Interactions Order +In notification-based flows, the available interaction types differ from those in dynamic link flows. Below are the interaction types allowed in notification-based flows: + +* `verificationCodeChoice` with `displayText60` +* `confirmationMessageAndVerificationCodeChoice` with `displayText200` + +### Example 1: confirmationMessageAndVerificationCodeChoice with Fallback to verificationCodeChoice +```java +NotificationSignatureSessionRequestBuilder builder = new NotificationSignatureSessionRequestBuilder(connector) + .withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessageAndVerificationCodeChoice("Confirm transaction of 1024.50 EUR"), + Interaction.verificationCodeChoice("Confirm transaction") + )); +``` + +### Example 2: verificationCodeChoice Only +```java +NotificationSignatureSessionRequestBuilder builder = new NotificationSignatureSessionRequestBuilder(connector) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Confirm transaction") + )); +``` + +## Response on Successful Notification-based Signature Session Creation +Upon successful initiation, the user will receive a notification on their Smart-ID app to complete the signing process. The response includes the `sessionID` and a `verificationCode` (Verification Code) object. + +## Response Parameters +* `sessionID`: Required. String used to request the operation result. +* `verificationCode`: Required. Object describing the Verification Code to be displayed. + * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. + * `value`: Required. Value of the VC code. + +## Error Handling +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: +* `UserAccountNotFoundException` +* `RelyingPartyAccountConfigurationException` +* `SessionNotFoundException` +* `RequiredInteractionNotSupportedByAppException` +* `ServerMaintenanceException` +* `SmartIdClientException` + +### Example of Error Handling +```java +try { + NotificationSignatureSessionResponse response = builder.initSignatureSession(); + + String sessionID = response.getSessionID(); + Vc verificationCode = response.getVc(); + + System.out.println("Session ID: " + sessionID); + System.out.println("Verification Code Type: " + verificationCode.getType()); + System.out.println("Verification Code Value: " + verificationCode.getValue()); + +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { + System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { + System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { + System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { + System.out.println("An error occurred: " + e.getMessage()); +} +``` + +## Additional Information +* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interaction it supports from the list. For notification-based flows, use `verificationCodeChoice` and `confirmationMessageAndVerificationCodeChoice`. +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm the transaction of 1024.50 EUR"), + Interaction.verificationCodeChoice("Confirm transaction") +)); +``` + +* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. +```java +var parameters = new RawDigestSignatureProtocolParameters(); +parameters.setDigest(signableData.calculateHashInBase64()); +parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); +builder.withSignatureProtocolParameters(parameters); +``` + +* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. +```java +var requestProperties = new RequestProperties(); +requestProperties.setShareMdClientIpAddress(true); +builder.withRequestProperties(requestProperties); +``` + +* `Nonce`: A random string up to 30 characters to associate the request with a specific session or transaction. +```java +builder.withNonce("randomNonce123"); +``` + +* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. +```java +builder.withCapabilities(Set.of("QUALIFIED", "ADVANCED")); +``` + +* `Certificate Level`: Set the required certificate level (`ADVANCED`, `QUALIFIED`, or `QSCD`). Defaults to `QUALIFIED`. +```java +builder.withCertificateLevel(CertificateLevel.QUALIFIED); +``` + +### Full Example +Here's a complete example of initiating a notification-based signature session: +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("your-relying-party-uuid"); +client.setRelyingPartyName("your-relying-party-name"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Prepare the signable data +SignableData signableData = new SignableData("Data to sign".getBytes()); +signableData.setHashType(HashType.SHA512); + +// Specify the Semantics Identifier or Document Number +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "31111111111" +); +// Or use document number +// String documentNumber = "PNOEE-31111111111-MOCK-Q"; + +// Build the notification signature request +NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) // or .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm the transaction of 1024.50 EUR"), + Interaction.verificationCodeChoice("Confirm transaction") + )) + .withNonce("randomNonce123") + .withShareMdClientIpAddress(true); + +// Initiate the notification signature session +NotificationSignatureSessionResponse response = builder.initSignatureSession(); + +// Process the response +String sessionID = response.getSessionID(); +Vc verificationCode = response.getVc(); + +System.out.println("Session ID: " + sessionID); +System.out.println("Verification Code Type: " + verificationCode.getType()); +System.out.println("Verification Code Value: " + verificationCode.getValue()); + +// Proceed with session status polling to obtain the signature +SessionStatusPoller poller = client.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionID); + +// Extract the signature from the session status +SmartIdSignature signature = SmartIdSignature.fromSessionStatus(sessionStatus); + +// Use the signature as needed +``` + ### Generating QR-code or dynamic link Todo: will be implemented in task SLIB-55 \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java index 0094a438..44a66e02 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java @@ -241,13 +241,13 @@ public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boole */ public DynamicLinkSignatureSessionResponse initSignatureSession() { validateParameters(); - DynamicLinkSignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); + SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); DynamicLinkSignatureSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); validateResponseParameters(dynamicLinkSignatureSessionResponse); return dynamicLinkSignatureSessionResponse; } - private DynamicLinkSignatureSessionResponse initSignatureSession(DynamicLinkSignatureSessionRequest request) { + private DynamicLinkSignatureSessionResponse initSignatureSession(SignatureSessionRequest request) { if (documentNumber != null) { return connector.initDynamicLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { @@ -257,8 +257,8 @@ private DynamicLinkSignatureSessionResponse initSignatureSession(DynamicLinkSign } } - private DynamicLinkSignatureSessionRequest createSignatureSessionRequest() { - var request = new DynamicLinkSignatureSessionRequest(); + private SignatureSessionRequest createSignatureSessionRequest() { + var request = new SignatureSessionRequest(); request.setRelyingPartyUUID(relyingPartyUUID); request.setRelyingPartyName(relyingPartyName); diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java new file mode 100644 index 00000000..8d10d9c4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java @@ -0,0 +1,363 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +public class NotificationSignatureSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); + + private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = + Set.of(InteractionFlow.DISPLAY_TEXT_AND_PIN, InteractionFlow.CONFIRMATION_MESSAGE); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List allowedInteractionsOrder; + private boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm; + private SignableData signableData; + private SignableHash signableHash; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public NotificationSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCapabilities(Set capabilities) { + this.capabilities = capabilities; + return this; + } + + /** + * Sets the allowed interactions order. + * + * @param allowedInteractionsOrder the allowed interactions order + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + return this; + } + + /** + * Ask to return the IP address of the mobile device where Smart-ID app was running. + * + * @return this builder + * @see Mobile Device IP sharing + */ + public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

        + * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + * If both {@link SignableData} and {@link SignableHash} are provided, {@link SignableData} will take precedence. + * + * @param signableData the data to be signed + * @return this builder instance + */ + public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + this.signableData = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

        + * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + * + * @param signableHash the hash data to be signed + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + this.signableHash = signableHash; + return this; + } + + /** + * Sends the signature request and initiates a notification-based signature session. + *

        + * There are two supported ways to start the signature session: + *

          + *
        • with a document number by using {@link #withDocumentNumber(String)}
        • + *
        • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
        • + *
        + * + * @return a {@link NotificationSignatureSessionResponse} containing session details such as + * session ID, session token, and session secret. + */ + public NotificationSignatureSessionResponse initSignatureSession() { + validateParameters(); + SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); + NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + validateResponseParameters(notificationSignatureSessionResponse); + return notificationSignatureSessionResponse; + } + + private NotificationSignatureSessionResponse initSignatureSession(SignatureSessionRequest request) { + if (documentNumber != null) { + return connector.initNotificationSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initNotificationSignature(request, semanticsIdentifier); + } else { + throw new IllegalArgumentException("Either documentNumber or semanticsIdentifier must be set."); + } + } + + private SignatureSessionRequest createSignatureSessionRequest() { + var request = new SignatureSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); + if (signableHash != null || signableData != null) { + signatureProtocolParameters.setDigest(getDigestToSignBase64()); + } + signatureProtocolParameters.setSignatureAlgorithm(getSignatureAlgorithm()); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setNonce(nonce); + request.setAllowedInteractionsOrder(allowedInteractionsOrder); + + if (this.shareMdClientIpAddress) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + request.setRequestProperties(requestProperties); + } + + request.setCapabilities(capabilities); + return request; + } + + private String getDigestToSignBase64() { + if (signableHash != null && signableHash.areFieldsFilled()) { + return signableHash.getHashInBase64(); + } else if (signableData != null) { + if (signableData.getHashType() == null) { + throw new SmartIdClientException("HashType must be set for signableData."); + } + return signableData.calculateHashInBase64(); + } else { + throw new SmartIdClientException("Either signableHash or signableData must be set."); + } + } + + private String getSignatureAlgorithm() { + if (signatureAlgorithm != null) { + return signatureAlgorithm.getAlgorithmName(); + } else if (signableHash != null && signableHash.getHashType() != null) { + return getSignatureAlgorithmName(signableHash.getHashType()); + } else if (signableData != null && signableData.getHashType() != null) { + return getSignatureAlgorithmName(signableData.getHashType()); + } else { + return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + } + } + + private String getSignatureAlgorithmName(HashType hashType) { + return switch (hashType) { + case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); + case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); + case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + }; + } + + private void validateParameters() { + if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { + throw new SmartIdClientException("Relying Party UUID must be set."); + } + if (relyingPartyName == null || relyingPartyName.isEmpty()) { + throw new SmartIdClientException("Relying Party Name must be set."); + } + validateAllowedInteractions(); + + if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { + throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); + } + } + + private void validateAllowedInteractions() { + if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); + } + Optional notSupportedInteraction = allowedInteractionsOrder.stream() + .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) + .findFirst(); + if (notSupportedInteraction.isPresent()) { + logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); + throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); + } + allowedInteractionsOrder.forEach(Interaction::validate); + } + + private void validateResponseParameters(NotificationSignatureSessionResponse response) { + if (StringUtil.isEmpty(response.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + + VerificationCode verificationCode = response.getVc(); + if (verificationCode == null) { + logger.error("VC object is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC object is missing from the response"); + } + + String vcType = verificationCode.getType(); + if (StringUtil.isEmpty(vcType)) { + logger.error("VC type is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC type is missing from the response"); + } + + if (!VerificationCode.ALPHA_NUMERIC_4.equals(vcType)) { + logger.error("Unsupported VC type: {}", vcType); + throw new UnprocessableSmartIdResponseException("Unsupported VC type: " + vcType); + } + + if (StringUtil.isEmpty(verificationCode.getValue())) { + logger.error("VC value is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC value is missing from the response"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java new file mode 100644 index 00000000..8d86a9e8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java @@ -0,0 +1,56 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class NotificationSignatureSessionResponse implements Serializable { + + private String sessionID; + + private VerificationCode vc; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public VerificationCode getVc() { + return vc; + } + + public void setVc(VerificationCode verificationCode) { + this.vc = verificationCode; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java rename to src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java index 842af0f8..83685067 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java @@ -34,7 +34,7 @@ import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.RequestProperties; -public class DynamicLinkSignatureSessionRequest implements Serializable { +public class SignatureSessionRequest implements Serializable { private String relyingPartyUUID; private String relyingPartyName; diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 59136259..bbc02654 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -93,6 +93,15 @@ public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { return new DynamicLinkSignatureSessionRequestBuilder(getSmartIdConnector()); } + /** + * Creates a new builder for creating a new notification signature session request + * + * @return builder for creating a new notification signature session request + */ + public NotificationSignatureSessionRequestBuilder createNotificationSignature() { + return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()); + } + /** * Sets the UUID of the relying party *

        diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index f0fe72ca..62c85a44 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,12 +32,13 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; +import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; @@ -75,7 +76,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a dynamic link based signature sessions. @@ -84,7 +85,25 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, String documentNumber); + DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); + + /** + * Initiates a notification-based signature session using a semantics identifier. + * + * @param request The notification signature session request containing the required parameters. + * @param semanticsIdentifier The semantics identifier for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a notification-based signature session using a document number. + * + * @param request The notification signature session request containing the required parameters. + * @param documentNumber The document number for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, String documentNumber); /** * Set the SSL context to use for secure communication diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index 63d1ac3b..fe4627cb 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -47,8 +47,9 @@ import ee.sk.smartid.rest.LoggingFilter; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; +import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -80,6 +81,9 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/dynamic-link/etsi"; private static final String DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/dynamic-link/document"; + private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; + private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; + private static final String ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH = "authentication/dynamic-link/anonymous"; private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/dynamic-link/document"; @@ -159,7 +163,7 @@ public DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateReq } @Override - public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -169,7 +173,7 @@ public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkS } @Override - public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkSignatureSessionRequest request, String documentNumber) { + public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) @@ -178,6 +182,24 @@ public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(DynamicLinkS return postSignatureRequest(uri, request); } + public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postNotificationSignatureRequest(uri, request); + } + + public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postNotificationSignatureRequest(uri, request); + } + @Override public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; @@ -253,7 +275,7 @@ private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI u } } - private DynamicLinkSignatureSessionResponse postSignatureRequest(URI uri, DynamicLinkSignatureSessionRequest request) { + private DynamicLinkSignatureSessionResponse postSignatureRequest(URI uri, SignatureSessionRequest request) { try { return postRequest(uri, request, DynamicLinkSignatureSessionResponse.class); } catch (NotFoundException ex) { @@ -265,6 +287,18 @@ private DynamicLinkSignatureSessionResponse postSignatureRequest(URI uri, Dynami } } + private NotificationSignatureSessionResponse postNotificationSignatureRequest(URI uri, SignatureSessionRequest request) { + try { + return postRequest(uri, request, NotificationSignatureSessionResponse.class); + } catch (NotFoundException ex) { + logger.warn("User account not found for URI {}", uri, ex); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } + } + private T postRequest(URI uri, V request, Class responseType) { try { Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java new file mode 100644 index 00000000..23112aed --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java @@ -0,0 +1,57 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class VerificationCode implements Serializable { + + public static final String ALPHA_NUMERIC_4 = "alphaNumeric4"; + + private String type; + + private String value; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java index 81bc2b78..f4bd9207 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -82,7 +82,7 @@ void initSignatureSession_withSemanticsIdentifier() { var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); builder.withSemanticsIdentifier(semanticsIdentifier); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); @@ -91,7 +91,7 @@ void initSignatureSession_withSemanticsIdentifier() { assertEquals("test-session-token", signature.getSessionToken()); assertEquals("test-session-secret", signature.getSessionSecret()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); @@ -102,7 +102,7 @@ void initSignatureSession_withDocumentNumber() { String documentNumber = "PNOEE-31111111111"; builder.withDocumentNumber(documentNumber); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); @@ -111,7 +111,7 @@ void initSignatureSession_withDocumentNumber() { assertEquals("test-session-token", signature.getSessionToken()); assertEquals("test-session-secret", signature.getSessionSecret()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(documentNumber)); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); @@ -122,15 +122,15 @@ void initSignatureSession_withDocumentNumber() { void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest request = requestCaptor.getValue(); + SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.getCertificateLevel()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); @@ -141,15 +141,15 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel void initSignatureSession_withNonce_ok(String nonce) { builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest request = requestCaptor.getValue(); + SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.getNonce()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); @@ -159,16 +159,16 @@ void initSignatureSession_withNonce_ok(String nonce) { void initSignatureSession_withRequestProperties() { builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertNotNull(capturedRequest.getRequestProperties()); assertTrue(capturedRequest.getRequestProperties().getShareMdClientIpAddress()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); @@ -180,17 +180,19 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { signableData.setHashType(HashType.SHA256); builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @ParameterizedTest @@ -201,14 +203,14 @@ void initSignatureSession_withSignableHash(HashType hashType) { signableHash.setHashType(hashType); builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); @@ -220,16 +222,16 @@ void initSignatureSession_withSignableHash(HashType hashType) { void initSignatureSession_withCapabilities(Set capabilities, Set expectedCapabilities) { builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @@ -241,14 +243,14 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT signableData.setHashType(hashType); builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA256WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); @@ -261,14 +263,15 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { signableData.setHashType(HashType.SHA512); builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockSignatureSessionResponse()); DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkSignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DynamicLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); } @@ -306,7 +309,7 @@ void initSignatureSession_whenCertificateChoiceMade() { void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlgorithm() { builder.withSignableHash(null).withSignableData(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); @@ -384,7 +387,7 @@ void validateResponseParameters_missingSessionID(String sessionID) { response.setSessionSecret("test-session-secret"); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session ID is missing from the response", ex.getMessage()); @@ -399,7 +402,7 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { response.setSessionSecret("test-session-secret"); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session token is missing from the response", ex.getMessage()); @@ -414,7 +417,7 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { response.setSessionSecret(sessionSecret); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(DynamicLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session secret is missing from the response", ex.getMessage()); diff --git a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java new file mode 100644 index 00000000..cecca2b1 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java @@ -0,0 +1,538 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +class NotificationSignatureSessionRequestBuilderTest { + + private SmartIdConnector connector; + private NotificationSignatureSessionRequestBuilder builder; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + + builder = new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withSignableData(new SignableData("Test data".getBytes())); + } + + @Test + void initSignatureSession_withSemanticsIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + builder.withSemanticsIdentifier(semanticsIdentifier); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.getSessionID()); + assertEquals("alphaNumeric4", signature.getVc().getType()); + assertEquals("4927", signature.getVc().getValue()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(semanticsIdentifier)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + } + + @Test + void initSignatureSession_withDocumentNumber() { + String documentNumber = "PNOEE-31111111111"; + builder.withDocumentNumber(documentNumber); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.getSessionID()); + assertEquals("alphaNumeric4", signature.getVc().getType()); + assertEquals("4927", signature.getVc().getValue()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(documentNumber)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { + builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.getNonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + } + + @Test + void withSignatureAlgorithm_setsCorrectAlgorithm() { + var signableData = new SignableData("Test data".getBytes()); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + } + + @Test + void initSignatureSession_withRequestProperties() { + builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.getRequestProperties()); + assertTrue(capturedRequest.getRequestProperties().getShareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void initSignatureSession_withSignableHash(HashType hashType) { + var signableHash = new SignableHash(); + signableHash.setHash("Test hash".getBytes()); + signableHash.setHashType(hashType); + builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities(Set capabilities, Set expectedCapabilities) { + builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(hashType); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA256WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + } + + @Test + void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(HashType.SHA512); + builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + } + + @Test + void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignableDataOrHash() { + builder.withSignableData(null).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + } + + @Nested + class ErrorCases { + + @Test + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { + builder.withDocumentNumber(null).withSemanticsIdentifier(null); + + var ex = assertThrows(IllegalArgumentException.class, () -> builder.initSignatureSession()); + assertEquals("Either documentNumber or semanticsIdentifier must be set.", ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableDataHashTypeIsNull() { + SignableData signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(null); + builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + SmartIdClientException exception = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("HashType must be set for signableData.", exception.getMessage()); + } + + @Test + void initSignatureSession_whenHashTypeIsNull() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(null); + builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("HashType must be set for signableData.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { + builder.withAllowedInteractionsOrder(allowedInteractionsOrder); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Allowed interactions order must be set and contain at least one interaction.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyUUID(String relyingPartyUUID) { + builder.withRelyingPartyUUID(relyingPartyUUID); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Relying Party UUID must be set.", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyName(String relyingPartyName) { + builder.withRelyingPartyName(relyingPartyName); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Relying Party Name must be set.", ex.getMessage()); + } + + @Test + void initSignatureSession_invalidNonce() { + builder.withNonce("1234567890123456789012345678901"); + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + } + + @Test + void initSignatureSession_emptyNonce() { + builder.withNonce(""); + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInteractionProvider.class) + void validateAllowedInteractions_containsUnsupportedInteraction(Interaction interaction, String expectedMessage) { + builder.withAllowedInteractionsOrder(List.of(interaction)); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals(expectedMessage, ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableHashNotFilled() { + var signableHash = new SignableHash(); + builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingSessionID(String sessionID) { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID(sessionID); + response.setVc(new VerificationCode()); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("Session ID is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeType(String vcType) { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + + VerificationCode verificationCode = new VerificationCode(); + verificationCode.setType(vcType); + response.setVc(verificationCode); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("VC type is missing from the response", ex.getMessage()); + } + + @Test + void validateResponse_unsupportedVerificationCodeType() { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + + VerificationCode vc = new VerificationCode(); + vc.setType("unsupportedType"); + response.setVc(vc); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("Unsupported VC type: unsupportedType", ex.getMessage()); + } + + @ParameterizedTest + @NullSource + void validateResponseParameters_missingVerificationCodeObject(VerificationCode vc) { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + response.setVc(vc); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("VC object is missing from the response", ex.getMessage()); + } + + @Test + void validateResponseParameters_emptyVerificationCode() { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + + VerificationCode emptyVc = new VerificationCode(); + response.setVc(emptyVc); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("VC type is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeValue(String vcValue) { + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + + VerificationCode vc = new VerificationCode(); + vc.setType("alphaNumeric4"); + vc.setValue(vcValue); + response.setVc(vc); + + when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("VC value is missing from the response", ex.getMessage()); + } + } + + private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { + var response = new NotificationSignatureSessionResponse(); + response.setSessionID("test-session-id"); + + var vc = new VerificationCode(); + vc.setType("alphaNumeric4"); + vc.setValue("4927"); + response.setVc(vc); + + return response; + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Set.of("QUALIFIED", "ADVANCED"), Set.of("QUALIFIED", "ADVANCED")), + Arguments.of(Set.of("QUALIFIED"), Set.of("QUALIFIED")), + Arguments.of(Set.of(), Set.of()) + ); + } + } + + private static class InvalidInteractionProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Interaction.displayTextAndPIN("Display text and PIN"), "AllowedInteractionsOrder contains not supported interaction DISPLAY_TEXT_AND_PIN"), + Arguments.of(Interaction.confirmationMessage("Confirmation message"), "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 122c4b1c..7c4c9b6d 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -167,4 +167,50 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } + + @Test + void createNotificationSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); + signableHash.setHashType(HashType.SHA512); + + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + + @Test + void createNotificationSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); + signableHash.setHashType(HashType.SHA512); + + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 388e26d9..2c705c90 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -61,8 +61,9 @@ import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionRequest; +import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; @@ -425,9 +426,9 @@ void getCertificate_throwsServerMaintenanceException() { @Nested @WireMockTest(httpPort = 18089) - class SignatureTests { + class DynamicLinkSignatureTests { - private SmartIdConnector connector; + private SmartIdRestConnector connector; @BeforeEach public void setUp() { @@ -439,7 +440,7 @@ public void setUp() { void initDynamicLinkSignature_withSemanticsIdentifier_successful() { stubPostRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); @@ -454,7 +455,7 @@ void initDynamicLinkSignature_withSemanticsIdentifier_successful() { void initDynamicLinkSignature_withDocumentNumber_successful() { stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111"; DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); @@ -469,7 +470,7 @@ void initDynamicLinkSignature_withDocumentNumber_successful() { void initDynamicLinkSignature_userAccountNotFound() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 404); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(UserAccountNotFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -479,7 +480,7 @@ void initDynamicLinkSignature_userAccountNotFound() { void initDynamicLinkSignature_relyingPartyNoPermission() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 403); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -489,7 +490,7 @@ void initDynamicLinkSignature_relyingPartyNoPermission() { void initDynamicLinkSignature_invalidRequest() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 400); - DynamicLinkSignatureSessionRequest request = new DynamicLinkSignatureSessionRequest(); + SignatureSessionRequest request = new SignatureSessionRequest(); request.setRelyingPartyUUID(""); request.setRelyingPartyName(""); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -501,7 +502,7 @@ void initDynamicLinkSignature_invalidRequest() { void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 401); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -513,7 +514,7 @@ void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_wh void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 471); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -523,7 +524,7 @@ void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcepti void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 472); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -533,7 +534,7 @@ void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { void initDynamicLinkSignature_throwsSmartIdClientException() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 480); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); @@ -545,13 +546,218 @@ void initDynamicLinkSignature_throwsSmartIdClientException() { void initDynamicLinkSignature_throwsServerMaintenanceException() { stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 580); - DynamicLinkSignatureSessionRequest request = createSignatureSessionRequest(); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(ServerMaintenanceException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); } } + @Nested + @WireMockTest(httpPort = 18084) + class SemanticsIdentifierNotificationSignature { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18084"); + } + + @Test + void initNotificationSignature() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + var semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, semanticsIdentifier); + + assertNotNull(response); + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + } + + @Test + void initNotificationSignature_requestIsUnauthorized_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 471); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 472); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse( + "/signature/notification/etsi/PNOEE-48010010101", 480); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 580); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); + connector.initNotificationSignature(request, semanticsIdentifier); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18085) + class DocumentNumberNotificationSignature { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18085"); + } + + @Test + void initNotificationSignature() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, documentNumber); + + assertNotNull(response); + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + } + + @Test + void initNotificationSignature_requestIsUnauthorized_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json"); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 471); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 472); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 480); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 580); + + SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> { + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + connector.initNotificationSignature(request, documentNumber); + }); + } + } + private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { var dynamicLinkAuthenticationSessionRequest = new DynamicLinkAuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); @@ -576,8 +782,8 @@ private CertificateRequest createCertificateRequest() { return request; } - private DynamicLinkSignatureSessionRequest createSignatureSessionRequest() { - var request = new DynamicLinkSignatureSessionRequest(); + private SignatureSessionRequest createSignatureSessionRequest() { + var request = new SignatureSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); @@ -591,4 +797,19 @@ private DynamicLinkSignatureSessionRequest createSignatureSessionRequest() { return request; } + + private SignatureSessionRequest createNotificationSignatureSessionRequest() { + var request = new SignatureSessionRequest(); + request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + request.setRelyingPartyName("DEMO"); + + var protocolParameters = new RawDigestSignatureProtocolParameters(); + protocolParameters.setDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="); + protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + + request.setSignatureProtocolParameters(protocolParameters); + request.setAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))); + + return request; + } } \ No newline at end of file diff --git a/src/test/resources/v3/requests/notification-signature-session-request.json b/src/test/resources/v3/requests/notification-signature-session-request.json new file mode 100644 index 00000000..34328744 --- /dev/null +++ b/src/test/resources/v3/requests/notification-signature-session-request.json @@ -0,0 +1,15 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "sha512WithRSAEncryption" + }, + "allowedInteractionsOrder": [ + { + "type": "verificationCodeChoice", + "displayText60": "Verify the code" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/notification-signature-session-response.json b/src/test/resources/v3/responses/notification-signature-session-response.json new file mode 100644 index 00000000..a84087a1 --- /dev/null +++ b/src/test/resources/v3/responses/notification-signature-session-response.json @@ -0,0 +1,7 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "vc": { + "type": "alphaNumeric4", + "value": "4927" + } +} \ No newline at end of file From d4f0df50c97f18e87050e7b948c29aff3547089f Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 18 Nov 2024 11:50:50 +0200 Subject: [PATCH 09/57] SLIB-55 - add generating dynamic content (#92) * SLIB-55 - add generating dynamic link * SLIB-55 - add generating hashed auth code * SLIB-55 - add generating QR-code * SLIB-55 - add license headers * SLIB-55 - update 3rd-party licenses * SLIB-55 - add description about generating dynamic link and QR-code * SLIB-55 - update generating of auth code; modified QR-code generation; update README.md * SLIB-55 - code review fixes; improve authCode validation logic in test * SLIB-55 - remove redundant test responses --- LICENSE.3RD-PARTY | 6 +- README.md | 141 +++++- pom.xml | 11 + src/main/java/ee/sk/smartid/v3/AuthCode.java | 98 ++++ .../sk/smartid/v3/DynamicContentBuilder.java | 207 +++++++++ .../ee/sk/smartid/v3/DynamicLinkType.java} | 38 +- .../ee/sk/smartid/v3/QrCodeGenerator.java | 142 ++++++ .../java/ee/sk/smartid/v3/SessionType.java | 47 ++ .../java/ee/sk/smartid/v3/SmartIdClient.java | 27 +- src/test/java/ee/sk/smartid/FileUtil.java | 2 +- ...ndpointSslVerificationIntegrationTest.java | 2 + .../java/ee/sk/smartid/v3/AuthCodeTest.java | 121 +++++ .../smartid/v3/DynamicContentBuilderTest.java | 235 ++++++++++ .../ee/sk/smartid/v3/QrCodeGeneratorTest.java | 205 +++++++++ .../java/ee/sk/smartid/v3/QrCodeUtil.java | 69 +++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 433 ++++++++++++------ .../v3/rest/SmartIdRestConnectorTest.java | 13 +- .../dynamicLinkCertificateChoiceResponse.json | 5 - .../dynamicLinkSignatureResponse.json | 5 - ...amic-link-certificate-choice-response.json | 2 +- 20 files changed, 1603 insertions(+), 206 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/v3/AuthCode.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java rename src/{test/java/ee/sk/smartid/v3/FileUtil.java => main/java/ee/sk/smartid/v3/DynamicLinkType.java} (59%) create mode 100644 src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java create mode 100644 src/main/java/ee/sk/smartid/v3/SessionType.java create mode 100644 src/test/java/ee/sk/smartid/v3/AuthCodeTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/QrCodeUtil.java delete mode 100644 src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json delete mode 100644 src/test/resources/v2/responses/dynamicLinkSignatureResponse.json diff --git a/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index 70cbfde8..83a6984f 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,7 +1,8 @@ -List of 108 third-party dependencies (auto-generated on 2024-09-27 with License Maven Plugin): +List of 112 third-party dependencies (auto-generated on 2024-11-04 with License Maven Plugin): * (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) * (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) +* (Apache License, Version 2.0) jcommander (com.beust:jcommander:1.82 - https://jcommander.org) * (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) * (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) * (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) @@ -11,6 +12,7 @@ List of 108 third-party dependencies (auto-generated on 2024-09-27 with License * (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) * (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) * (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) +* (BSD 3-clause License w/nuclear disclaimer) Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core) * (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) @@ -22,6 +24,8 @@ List of 108 third-party dependencies (auto-generated on 2024-09-27 with License * (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) * (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) * (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) +* (The Apache Software License, Version 2.0) ZXing Core (com.google.zxing:core:3.5.3 - https://github.com/zxing/zxing/core) +* (The Apache Software License, Version 2.0) ZXing Java SE extensions (com.google.zxing:javase:3.5.3 - https://github.com/zxing/zxing/javase) * (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) * (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) * (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) diff --git a/README.md b/README.md index 9af1fcea..04db32b1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Requirements](#requirements) * [Getting the library](#getting-the-library) * [Changelog](#changelog) -* [How to use it](#how-to-use-it) +* [How to use it with RP API v2.0](#how-to-use-api-v20) * [Test accounts for testing]() * [Logging](#logging) * [Log request payloads](#log-request-payloads) @@ -45,6 +45,14 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Configuring a proxy](#configuring-a-proxy) * [Configuring a proxy using JBoss Resteasy library](#configuring-a-proxy-using-jboss-resteasy-library) * [Configuring a proxy using Jersey](#configuring-a-proxy-using-jersey) +* [How to use it with RP API v3.0](#how-to-use-api-v30) + * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) + * [Generating dynamic link ](#generating-dynamic-link) + * [Dynamic link parameters](#dynamic-link-parameters) + * [Overriding default values](#overriding-default-values) + * [Generating QR-code](#generating-qr-code) + * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) + ## Introduction @@ -968,17 +976,17 @@ client.createDynamicLinkCertificateRequest().withShareMdClientIpAddress(true); ### Example of Initiating a dynamic link certificate choice request with `QUALIFIED` certificate level and IP sharing enabled. ```java SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withShareMdClientIpAddress(true) - .initiateCertificateChoice(); +DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withShareMdClientIpAddress(true) + .initiateCertificateChoice(); ``` # Initiating a Dynamic Link Signature Session in API v3.0 @@ -1479,4 +1487,113 @@ SmartIdSignature signature = SmartIdSignature.fromSessionStatus(sessionStatus); ``` ### Generating QR-code or dynamic link -Todo: will be implemented in task SLIB-55 \ No newline at end of file +Todo: will be implemented in task SLIB-55 +## Generating QR-code or dynamic link + +#### Generating dynamic link + +Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. +Providing QR-code as a dynamic link type will allow generating QR-code at frontend side. + +##### Dynamic link parameters + +* `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. +* `version`: Version of the dynamic link. Default value is `0.1`. +* `dynamicLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. +* `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. +* `sessionToken`: Token from the session response. +* `elapsedTime`: Elapsed time from when the session response was received. +* `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link +* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType and current time and session secret. Session secret can be found in the session response. + +```java +DynamicLinkAuthenticationSessionResponse response; // response from the session initiation query. +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI dynamicLink = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createUri(); +``` + +##### Overriding default values + +```java +DynamicLinkAuthenticationSessionResponse response; // response from the session initiation query. +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI dynamicLink = client.createDynamicContent() + .withBaseUrl("https://example.com") // override default base URL (https://smart-id.com/dynamic-link) + .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withUserLanguage("est") // override default user language (eng) + .withAuthCode(authCode) + .createUri(); +``` + +#### Generating QR-code + +Creating a QR code uses the Zxing library to generate a QR code image with dynamic link as content. +According to link size the QR-code of version 9 (53x53 modules) is used. +For the QR-code to be scannable by most devices the QR code module size should be 10px. +It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). +Generated QR code will have error correction level low. + +##### Generate QR-code Data URI + +```java +DynamicLinkAuthenticationSessionResponse response; // init auth sessions response +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) +String qrCodeDataUri = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createQrCode(); +``` + +##### Generate QR-code with custom height, width, quiet area and image format + +Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. +QR code version 9 (53x53 modules) is automatically selected by content size + +Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. +The width and height of 1159px produce a QR code with a module size of 19px. + +```java +DynamicLinkAuthenticationSessionResponse response; // init auth sessions response +// Capture and store when initiating session response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI qrDataUri = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for, possible values (auth, sign, cert) + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createUri(); + +// Generate QR-code with height and width of 570px and quiet area of 2 modules. +BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(qrDataUri, 570, 570, 2); + +// Convert BufferedImage to Data URI +String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5e7ef86b..0b78f58e 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,17 @@ 1.78.1 + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + + org.junit.jupiter junit-jupiter-api diff --git a/src/main/java/ee/sk/smartid/v3/AuthCode.java b/src/main/java/ee/sk/smartid/v3/AuthCode.java new file mode 100644 index 00000000..4435772a --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/AuthCode.java @@ -0,0 +1,98 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * This class is responsible for creating an authentication code hash for the dynamic link. + */ +public final class AuthCode { + + private static final String PAYLOAD_FORMAT = "%s.%s.%d"; + + private AuthCode() { + } + + /** + * Creates an authentication code hash for the dynamic link with the given time. + * + * @param dynamicLinkType the type of the dynamic link @{@link DynamicLinkType} + * @param sessionType the type of the session @{@link SessionType} + * @param sessionSecret the session secret + * @param elapsedSeconds the time from session creation response was received + * @return the authentication code in Base64 URL safe format + */ + public static String createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String sessionSecret, long elapsedSeconds) { + validateInputs(dynamicLinkType, sessionType, sessionSecret); + String payload = createPayload(dynamicLinkType, sessionType, elapsedSeconds); + return hashThePayload(payload, sessionSecret); + } + + /** + * Hashes the payload with the session secret. + * + * @param payload the payload to be hashed + * @param sessionSecret the secret of the session + * @return the hashed payload in Base64 URL safe format + */ + public static String hashThePayload(String payload, String sessionSecret) { + HMac hmac = new HMac(new SHA256Digest()); + hmac.init(new KeyParameter(sessionSecret.getBytes(StandardCharsets.UTF_8))); + + byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + hmac.update(payloadBytes, 0, payloadBytes.length); + + byte[] result = new byte[hmac.getMacSize()]; + hmac.doFinal(result, 0); + + return Base64.getUrlEncoder().withoutPadding().encodeToString(result); + } + + private static void validateInputs(DynamicLinkType dynamicLinkType, SessionType sessionType, String sessionSecret) { + if (dynamicLinkType == null) { + throw new SmartIdClientException("Dynamic link type must be set"); + } + if (sessionType == null) { + throw new SmartIdClientException("Session type must be set"); + } + if (sessionSecret == null) { + throw new SmartIdClientException("Session secret must be set"); + } + } + + private static String createPayload(DynamicLinkType dynamicLinkType, SessionType sessionType, long elapsedSeconds) { + return String.format(PAYLOAD_FORMAT, dynamicLinkType.getValue(), sessionType.getValue(), elapsedSeconds); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java new file mode 100644 index 00000000..dbba481e --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java @@ -0,0 +1,207 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Builds dynamic content. Can be used to generate dynamic link or QR code. + */ +public class DynamicContentBuilder { + + private static final String DEFAULT_BASE_URL = "https://smart-id.com/dynamic-link/"; + private static final String DEFAULT_VERSION = "0.1"; + private static final String DEFAULT_USER_LANGUAGE = "eng"; + + private String baseUrl = DEFAULT_BASE_URL; + private String version = DEFAULT_VERSION; + private DynamicLinkType dynamicLinkType; + private SessionType sessionType; + private String sessionToken; + private Long elapsedSeconds; + private String userLanguage = DEFAULT_USER_LANGUAGE; + private String authCode; + + /** + * Sets the URL + *

        + * Defaults to https://smart-id.com/dynamic-link + * + * @param baseUrl the URL that will direct to SMART-ID application + * @return this builder + */ + public DynamicContentBuilder withBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Sets the version of the dynamic link. + *

        + * Defaults to 0.1 + * + * @param version the version of + * @return this builder + */ + public DynamicContentBuilder withVersion(String version) { + this.version = version; + return this; + } + + /** + * Sets the type of the dynamic link. Use {@link DynamicLinkType} to set the type. + * + * @param dynamicLinkType the type of the dynamic link the builder is creating + * @return this builder + */ + public DynamicContentBuilder withDynamicLinkType(DynamicLinkType dynamicLinkType) { + this.dynamicLinkType = dynamicLinkType; + return this; + } + + /** + * Sets the type of the session. Use {@link SessionType} to set the type. + * + * @param sessionType the type of the session the dynamic link is created for + * @return this builder + */ + public DynamicContentBuilder withSessionType(SessionType sessionType) { + this.sessionType = sessionType; + return this; + } + + /** + * Sets the session token that was received from the Smart-ID server. + * + * @param sessionToken the session token that was received from the Smart-ID server + * @return this builder + */ + public DynamicContentBuilder withSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + /** + * Sets the time passed since the session response was received. + * + * @param elapsedSeconds the time passed since the session response was received in seconds + * @return this builder + */ + public DynamicContentBuilder withElapsedSeconds(Long elapsedSeconds) { + this.elapsedSeconds = elapsedSeconds; + return this; + } + + /** + * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. + *

        + * Defaults to "eng" + * + * @param userLanguage the language of the user + * @return this builder + */ + public DynamicContentBuilder withUserLanguage(String userLanguage) { + this.userLanguage = userLanguage; + return this; + } + + /** + * Sets the auth code that will be used in the dynamic link. + * + * @param authCode the auth code in the dynamic link + * @return this builder + */ + public DynamicContentBuilder withAuthCode(String authCode) { + this.authCode = authCode; + return this; + } + + /** + * Creates a URI that can be used as dynamic link or content for QR-code. + *

        + * To get a QR code image, use {@link #createQrCodeDataUri()} method. + * + * @return URI that can be used as dynamic link or content for QR-code + */ + public URI createUri() { + validateInputParameters(); + return UriBuilder.fromUri(baseUrl) + .queryParam("version", version) + .queryParam("sessionToken", sessionToken) + .queryParam("dynamicLinkType", dynamicLinkType.getValue()) + .queryParam("sessionType", sessionType.getValue()) + .queryParam("elapsedSeconds", elapsedSeconds) + .queryParam("lang", userLanguage) + .queryParam("authCode", authCode) + .build(); + } + + /** + * Creates a QR code image as a Base64 encoded string. + *

        + * The dynamic link type must be QR_CODE to create a QR code image. + * + * @return QR code image as a Base64 encoded string + */ + public String createQrCodeDataUri() { + if (dynamicLinkType != DynamicLinkType.QR_CODE) { + throw new SmartIdClientException("Dynamic link type must be QR_CODE"); + } + return QrCodeGenerator.generateDataUri(createUri().toString()); + } + + private void validateInputParameters() { + if (StringUtil.isEmpty(baseUrl)) { + throw new SmartIdClientException("Parameter baseUrl must be set"); + } + if (StringUtil.isEmpty(version)) { + throw new SmartIdClientException("Parameter version must be set"); + } + if (dynamicLinkType == null) { + throw new SmartIdClientException("Parameter dynamicLinkType must be set"); + } + if (sessionType == null) { + throw new SmartIdClientException("Parameter sessionType must be set"); + } + if (StringUtil.isEmpty(sessionToken)) { + throw new SmartIdClientException("Parameter sessionToken must be set"); + } + if (elapsedSeconds == null) { + throw new SmartIdClientException("Parameter elapsedSeconds must be set"); + } + if (StringUtil.isEmpty(userLanguage)) { + throw new SmartIdClientException("Parameter userLanguage must be set"); + } + if (StringUtil.isEmpty(authCode)) { + throw new SmartIdClientException("Parameter authCode must be set"); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/FileUtil.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkType.java similarity index 59% rename from src/test/java/ee/sk/smartid/v3/FileUtil.java rename to src/main/java/ee/sk/smartid/v3/DynamicLinkType.java index d2137b66..fc195430 100644 --- a/src/test/java/ee/sk/smartid/v3/FileUtil.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkType.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,30 +26,22 @@ * #L% */ -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; +/** + * Enum for dynamic link types + */ +public enum DynamicLinkType { -public final class FileUtil { + QR_CODE("QR"), + WEB_2_APP("Web2App"), + APP_2_APP("App2App"); - private FileUtil() { - } + private final String value; - public static String readFileToString(String fileName) { - return new String(readFileBytes(fileName), StandardCharsets.UTF_8); + DynamicLinkType(String value) { + this.value = value; } - private static byte[] readFileBytes(String fileName) { - try { - ClassLoader classLoader = FileUtil.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull(resource, "File not found: " + fileName); - return Files.readAllBytes(Paths.get(resource.toURI())); - } catch (Exception e) { - throw new RuntimeException("Exception: " + e.getMessage(), e); - } + public String getValue() { + return value; } -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java b/src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java new file mode 100644 index 00000000..0c310561 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java @@ -0,0 +1,142 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; +import static com.google.zxing.EncodeHintType.MARGIN; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * This class is responsible for generating QR-codes. + * It can generate QR-codes as Data URIs or as BufferedImages. + *

        + * The default image size of the generated QR code is 610x610px. + * It is calculated based on the version 9 QR-code that contains 53x53 modules and four quiet area modules. + * QR-code version 9 is selected automatically based on the provided length of the data. + * The module size should be 10px, so the image size is 53x10=530px + 2x4x10=80px = 610px. + *

        + * Generated QR-codes have LOW error correction level. + */ +public class QrCodeGenerator { + + private static final int DEFAULT_QR_CODE_WIDTH_PX = 610; + private static final int DEFAULT_QR_CODE_HEIGHT = 610; + private static final int DEFAULT_QUIET_AREA_SIZE_MODULES = 4; + private static final String DEFAULT_FILE_FORMAT = "png"; + + /** + * Generates a QR-code as Data URI + *

        + * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a Base64 encoded string + */ + public static String generateDataUri(String data) { + BufferedImage bufferedImage = generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + return convertToDataUri(bufferedImage, DEFAULT_FILE_FORMAT); + } + + /** + * Generates a QR-code as BufferedImage + *

        + * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data) { + return generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + } + + /** + * Generates a QR-code as BufferedImage. + *

        + * Provide the width and height of the image in pixels and the size of the quiet area around the QR-code in modules. + * + * @param data the data to be encoded + * @param widthPx the width of the image in pixels + * @param heightPx the height of the image in pixels + * @param quietAreaSize the size of the quiet area around the QR-code, value in modules + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data, int widthPx, int heightPx, int quietAreaSize) { + if (data == null || data.isEmpty()) { + throw new SmartIdClientException("Provided data cannot be empty"); + } + BitMatrix matrix; + try { + Map hints = new HashMap<>(); + hints.put(MARGIN, quietAreaSize); + hints.put(ERROR_CORRECTION, ErrorCorrectionLevel.L); + + matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, widthPx, heightPx, hints); + } catch (WriterException ex) { + throw new SmartIdClientException("Unable to create QR-code", ex); + } + return MatrixToImageWriter.toBufferedImage(matrix); + } + + /** + * Converts provided BufferedImage to Data URI with provided file format. + * + * @param bufferedImage the image to be converted + * @param fileFormat the format of the image + * @return the image as a Data URI + */ + public static String convertToDataUri(BufferedImage bufferedImage, String fileFormat) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, fileFormat, outputStream); + String imgBase64 = Base64.getMimeEncoder().encodeToString(outputStream.toByteArray()); + return toDataUri(imgBase64, fileFormat); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to generate QR-code", ex); + } + } + + private static String toDataUri(String imageData, String fileFormat) { + return String.format("data:image/%s;base64,%s", fileFormat, imageData); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SessionType.java b/src/main/java/ee/sk/smartid/v3/SessionType.java new file mode 100644 index 00000000..85fce0c1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SessionType.java @@ -0,0 +1,47 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Enum for session types + */ +public enum SessionType { + + AUTHENTICATION("auth"), + SIGNATURE("sign"), + CERTIFICATE_CHOICE("cert"); + + private final String value; + + SessionType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index bbc02654..b09eed6e 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -102,6 +102,26 @@ public NotificationSignatureSessionRequestBuilder createNotificationSignature() return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()); } + /** + * Create a new Smart-ID session status poller + * + * @return Sessions status poller + */ + public SessionStatusPoller createSessionStatusPoller() { + var sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + return sessionStatusPoller; + } + + /** + * Create builder for generating dynamic link or QR-code + * + * @return DynamicLinkRequestBuilder + */ + public DynamicContentBuilder createDynamicContent() { + return new DynamicContentBuilder(); + } + /** * Sets the UUID of the relying party *

        @@ -271,13 +291,6 @@ public void setSmartIdConnector(SmartIdConnector smartIdConnector) { this.connector = smartIdConnector; } - private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - var sessionStatusPoller = new SessionStatusPoller(connector); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); - return sessionStatusPoller; - } - private Client createClient() { ClientBuilder clientBuilder = ClientBuilder.newBuilder(); if (networkConnectionConfig != null) { diff --git a/src/test/java/ee/sk/smartid/FileUtil.java b/src/test/java/ee/sk/smartid/FileUtil.java index f1429492..20ad20ae 100644 --- a/src/test/java/ee/sk/smartid/FileUtil.java +++ b/src/test/java/ee/sk/smartid/FileUtil.java @@ -42,7 +42,7 @@ public static String readFileToString(String fileName) { return new String(readFileBytes(fileName), StandardCharsets.UTF_8); } - private static byte[] readFileBytes(String fileName) { + public static byte[] readFileBytes(String fileName) { try { ClassLoader classLoader = FileUtil.class.getClassLoader(); URL resource = classLoader.getResource(fileName); diff --git a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java index d556c11f..e83ce6fe 100644 --- a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java @@ -43,12 +43,14 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.FileUtil; +import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.integration.SmartIdIntegrationTest; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; +@SmartIdDemoIntegrationTest public class EndpointSslVerificationIntegrationTest { private static final String DEMO_HOST_URL = "https://sid.demo.sk.ee/smart-id-rp/v2/"; diff --git a/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java b/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java new file mode 100644 index 00000000..04aa1fc4 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java @@ -0,0 +1,121 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.stream.Stream; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class AuthCodeTest { + + @ParameterizedTest + @ArgumentsSource(AuthCodeArgumentsProvider.class) + void createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String expectedPayload) { + String authCodeInBase64 = AuthCode.createHash(dynamicLinkType, sessionType, "sessionSecret", 1); + + String expected = hashThePayload(expectedPayload); + assertEquals(expected, authCodeInBase64); + } + + @Test + void createHash_dynamicLinkTypeNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(null, SessionType.AUTHENTICATION, "sessionSecret", 1)); + assertEquals("Dynamic link type must be set", ex.getMessage()); + } + + @Test + void createHash_sessionTypeNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, null, "sessionSecret", 1)); + assertEquals("Session type must be set", ex.getMessage()); + } + + @Test + void createHash_sessionSecretNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, null, 1)); + assertEquals("Session secret must be set", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"QR.auth.1", "QR.sign.1", "QR.cert.1"}) + void hashThePayload_validateUrlSafe(String payload) { + String authCodeHash = AuthCode.hashThePayload(payload, "sessionSecret"); + String urlSafeBase64Pattern = "^[A-Za-z0-9_-]+={0,2}$"; + assertTrue(authCodeHash.matches(urlSafeBase64Pattern)); + assertEquals(hashThePayload(payload), authCodeHash); + } + + private String hashThePayload(String payload) { + try { + byte[] keyBytes = "sessionSecret".getBytes(StandardCharsets.UTF_8); + SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(secretKeySpec); + byte[] data = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(data); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static class AuthCodeArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, "QR.auth.1"), + Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.AUTHENTICATION, "Web2App.auth.1"), + Arguments.of(DynamicLinkType.APP_2_APP, SessionType.AUTHENTICATION, "App2App.auth.1"), + + Arguments.of(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, "QR.sign.1"), + Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.SIGNATURE, "Web2App.sign.1"), + Arguments.of(DynamicLinkType.APP_2_APP, SessionType.SIGNATURE, "App2App.sign.1"), + + Arguments.of(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, "QR.cert.1"), + Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.CERTIFICATE_CHOICE, "Web2App.cert.1"), + Arguments.of(DynamicLinkType.APP_2_APP, SessionType.CERTIFICATE_CHOICE, "App2App.cert.1") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java new file mode 100644 index 00000000..0afe6727 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java @@ -0,0 +1,235 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class DynamicContentBuilderTest { + + @Nested + class CreateUri { + + @ParameterizedTest + @EnumSource + void createUri_forDifferentDynamicLinks(DynamicLinkType dynamicLinkType) { + long elapsedSeconds = 1L; + URI uri = new DynamicContentBuilder() + .withBaseUrl("https://smart-id.com/dynamic-link/") + .withVersion("0.1") + .withDynamicLinkType(dynamicLinkType) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("sessionToken") + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, "sessionSecret", elapsedSeconds)) + .createUri(); + + assertUri(uri, dynamicLinkType, SessionType.AUTHENTICATION); + } + + @ParameterizedTest + @EnumSource + void createUri_withSessionType(SessionType sessionType) { + long elapsedSeconds = 1L; + URI uri = new DynamicContentBuilder() + .withBaseUrl("https://smart-id.com/dynamic-link/") + .withVersion("0.1") + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(sessionType) + .withSessionToken("sessionToken") + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, "sessionSecret", elapsedSeconds)) + .createUri(); + + assertUri(uri, DynamicLinkType.QR_CODE, sessionType); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_baseUrlIsOverriddenToBeEmpty_throwException(String baseUrl) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withBaseUrl(baseUrl) + .createUri()); + assertEquals("Parameter baseUrl must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_versionIsOverriddenToBeEmpty_throwException(String version) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withVersion(version) + .createUri()); + assertEquals("Parameter version must be set", ex.getMessage()); + } + + @Test + void createUri_dynamicLinkTypeIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(null) + .createUri()); + assertEquals("Parameter dynamicLinkType must be set", ex.getMessage()); + } + + @Test + void createUri_sessionTypeIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(null) + .createUri()); + assertEquals("Parameter sessionType must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_sessionTokenIsEmpty_throwException(String sessionToken) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .createUri()); + assertEquals("Parameter sessionToken must be set", ex.getMessage()); + } + + @Test + void createUri_elapsedSecondsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("sessionToken") + .withElapsedSeconds(null) + .createUri()); + assertEquals("Parameter elapsedSeconds must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_userLanguageIsEmpty_throwException(String userLanguage) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("sessionToken") + .withElapsedSeconds(1L) + .withUserLanguage(userLanguage) + .createUri()); + assertEquals("Parameter userLanguage must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_authCodeIsEmpty_throwException(String authCode) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("sessionToken") + .withElapsedSeconds(1L) + .withAuthCode(authCode) + .createUri()); + assertEquals("Parameter authCode must be set", ex.getMessage()); + } + } + + @Nested + class CreateQrCode { + + @ParameterizedTest + @EnumSource + void createQrCode_forDifferentSessionsTypes(SessionType sessionType) { + String qrDataUri = new DynamicContentBuilder() + .withBaseUrl("https://smart-id.com/dynamic-link/") + .withVersion("0.1") + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(sessionType) + .withSessionToken("sessionToken") + .withElapsedSeconds(1L) + .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, "sessionSecret", 1)) + .createQrCodeDataUri(); + + String[] qrDataUriParts = qrDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrDataUriParts[1]).getText()); + assertUri(uri, DynamicLinkType.QR_CODE, sessionType); + } + + @ParameterizedTest + @EnumSource(value = DynamicLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createQrCode_wrongLinkTypeIsBeingUsed_throwException(DynamicLinkType notSupportedDynamicLinkType) { + var ex = assertThrows(SmartIdClientException.class, + () -> new DynamicContentBuilder() + .withDynamicLinkType(notSupportedDynamicLinkType) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("sessionToken") + .withElapsedSeconds(1L) + .withAuthCode("authCode") + .createQrCodeDataUri()); + assertEquals("Dynamic link type must be QR_CODE", ex.getMessage()); + } + } + + private static void assertUri(URI uri, DynamicLinkType qrCode, SessionType sessionType) { + assertThat(uri.getScheme(), equalTo("https")); + assertThat(uri.getHost(), equalTo("smart-id.com")); + assertThat(uri.getPath(), equalTo("/dynamic-link/")); + + Map queryParams = toQueryParamsMap(uri); + assertThat(queryParams, hasEntry("version", "0.1")); + assertThat(queryParams, hasEntry("dynamicLinkType", qrCode.getValue())); + assertThat(queryParams, hasEntry("sessionType", sessionType.getValue())); + assertThat(queryParams, hasEntry("sessionToken", "sessionToken")); + assertThat(queryParams, hasEntry("elapsedSeconds", "1")); + assertThat(queryParams, hasEntry(equalTo("authCode"), matchesPattern("^[A-Za-z0-9_-]+={0,2}$"))); + } + + private static Map toQueryParamsMap(URI uri) { + return Arrays.stream(uri.getQuery().split("&")) + .map(param -> param.split("=")) + .collect(Collectors.toMap(param -> param[0], param -> param[1])); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java new file mode 100644 index 00000000..b02a3f93 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java @@ -0,0 +1,205 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.Base64; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class QrCodeGeneratorTest { + + private static final int WHITE_COLOR = -1; + + @Test + void generateDataUri_validateQrContent() { + URI uri = createUri(); + String base64ImageData = QrCodeGenerator.generateDataUri(uri.toString()); + + assertNotNull(base64ImageData); + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + assertEquals(uri.toString(), QrCodeUtil.extractQrContent(parts[1]).getText()); + } + + @Nested + class DefaultValues { + + @Test + void generateDataUri() { + URI uri = createUri(); + String qrDataUri = QrCodeGenerator.generateDataUri(uri.toString()); + String imgBase64 = qrDataUri.split(",")[1]; + BufferedImage qrImage = convertToBufferedImage(imgBase64); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + + @Test + void generateBufferedImage() { + URI uri = createUri(); + BufferedImage qrImage = QrCodeGenerator.generateImage(uri.toString()); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + } + + @Test + void generateImage_providedCustomValues() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 100, 100, quietAreaSize); + + assertEquals(100, bufferedImage.getHeight()); + assertEquals(100, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + float expectedModuleSize = (float) bufferedImage.getWidth() / (53 + 2 * quietAreaSize); + assertQrModuleSize(bufferedImage, 2, expectedModuleSize); + } + + @Nested + class QrCodeModulePixelRanges { + + @Test + void generateImage_providedCustomValues_moduleSize6px() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 366, 366, quietAreaSize); + + assertEquals(366, bufferedImage.getHeight()); + assertEquals(366, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 6); + } + + @Test + void generateImage_providedCustomValues_moduleSize19px() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 1159, 1159, 4); + + assertEquals(1159, bufferedImage.getHeight()); + assertEquals(1159, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 19); + } + } + + @ParameterizedTest + @NullAndEmptySource + void generateImage_providedDataIsEmpty_throwException(String data) { + var ex = assertThrows(SmartIdClientException.class, () -> QrCodeGenerator.generateImage(data, 10, 10, 2)); + + assertEquals("Provided data cannot be empty", ex.getMessage()); + } + + @Test + void convertToBase64() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString()); + String base64ImageData = QrCodeGenerator.convertToDataUri(bufferedImage, "png"); + + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + Pattern pattern = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); + assertTrue(pattern.matcher(parts[1].replaceAll("\\s", "")).matches()); + } + + private static URI createUri() { + return new DynamicContentBuilder() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") + .withAuthCode("Y7jBVqtP_KcY4GyJ0gTK717wZnfRLvondEUjjCRJAsQ") + .withElapsedSeconds(1L) + .createUri(); + } + + private static BufferedImage convertToBufferedImage(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream bis = new ByteArrayInputStream(qrCodeBytes)) { + return ImageIO.read(bis); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static boolean validateQuietArea(BufferedImage qrImage, int quietZoneModules, int moduleSize) { + int quietZonePixelSize = quietZoneModules * moduleSize; + + // Validate top and bottom quiet areas + for (int y = 0; y < quietZonePixelSize; y++) { + for (int x = 0; x < qrImage.getWidth(); x++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(x, qrImage.getHeight() - 1 - y) != WHITE_COLOR) { + return false; + } + } + } + // Validate left and right quiet areas + for (int x = 0; x < quietZonePixelSize; x++) { + for (int y = 0; y < qrImage.getHeight(); y++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(qrImage.getWidth() - 1 - x, y) != WHITE_COLOR) { + return false; + } + } + } + return true; + } + + private static void assertQrModuleSize(BufferedImage qrImage, + int nrOfQuietAreaModules, + float expectedModuleSizePx) { + float qrCodeWidth = 53 * expectedModuleSizePx; + float quiteAreaWidth = nrOfQuietAreaModules * expectedModuleSizePx; + float expectedWidth = qrCodeWidth + 2 * quiteAreaWidth; + assertEquals(expectedWidth, qrImage.getWidth()); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/QrCodeUtil.java b/src/test/java/ee/sk/smartid/v3/QrCodeUtil.java new file mode 100644 index 00000000..185f1d95 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/QrCodeUtil.java @@ -0,0 +1,69 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; + +import javax.imageio.ImageIO; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.multi.qrcode.QRCodeMultiReader; + +public class QrCodeUtil { + + private QrCodeUtil() { + } + + public static Result extractQrContent(String qrDataUri) { + BinaryBitmap bitmap = getBinaryBitmap(qrDataUri); + Result result; + try { + result = Arrays.stream(new QRCodeMultiReader().decodeMultiple(bitmap)).findFirst().get(); + } catch (NotFoundException ex) { + throw new RuntimeException(ex); + } + return result; + } + + public static BinaryBitmap getBinaryBitmap(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes)) { + BufferedImage bufferedImage = ImageIO.read(inputStream); + return new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 7c4c9b6d..19986118 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -26,22 +26,31 @@ * #L% */ +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.List; import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.FileUtil; import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SessionStatus; -@WireMockTest(httpPort = 18089) class SmartIdClientTest { private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); @@ -57,160 +66,296 @@ void setUp() { smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); } - @Test - void createDynamicLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - - DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initiateCertificateChoice(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); + @Nested + @WireMockTest(httpPort = 18089) + class DynamicLinkCertificateChoiceSession { + + @Test + void createDynamicLinkCertificateChoice() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + + DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initiateCertificateChoice(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } } - @Test - void createDynamicLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); + @Nested + @WireMockTest(httpPort = 18089) + class DynamicLinkAuthenticationSession { + + @Test + void createDynamicLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } } - @Test - void createDynamicLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); + @Nested + @WireMockTest(httpPort = 18089) + class DynamicLinkSignatureSession { + + @Test + void createDynamicLinkSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } + + @Test + void createDynamicLinkSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getSessionToken()); + assertNotNull(response.getSessionSecret()); + } } - @Test - void createDynamicLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - } - @Test - void createDynamicLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); - - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); - - DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); + @Nested + @WireMockTest(httpPort = 18089) + class NotificationBasedSignatureSession { + @Test + void createNotificationSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); + signableHash.setHashType(HashType.SHA512); + + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + + @Test + void createNotificationSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); + signableHash.setHashType(HashType.SHA512); + + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } } - @Test - void createDynamicLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); - - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); - - DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - } + @Nested + @WireMockTest(httpPort = 18089) + class SessionsStatus { + + @Test + void fetchFinalSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + + SessionStatus status = smartIdClient.createSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); - @Test - void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); - - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); - signableHash.setHashType(HashType.SHA512); - - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertEquals("COMPLETE", status.getState()); + assertEquals("OK", status.getResult().getEndResult()); + } } - @Test - void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); - - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); - signableHash.setHashType(HashType.SHA512); - - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContent { + + @ParameterizedTest + @EnumSource + void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); + Instant sessionResponseReceivedTime = Instant.now(); + + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, response.getSessionSecret(), 1); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(dynamicLinkType) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.getSessionToken()) + .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withUserLanguage("eng") + .withAuthCode(authCode) + .createUri(); + + assertUri(qrCodeUri, SessionType.AUTHENTICATION, dynamicLinkType, response.getSessionToken()); + } + + + @ParameterizedTest + @EnumSource + void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + + DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initiateCertificateChoice(); + Instant sessionResponseReceivedTime = Instant.now(); + + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(dynamicLinkType) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.getSessionToken()) + .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withUserLanguage("eng") + .withAuthCode(authCode) + .createUri(); + + assertUri(qrCodeUri, SessionType.CERTIFICATE_CHOICE, dynamicLinkType, response.getSessionToken()); + } + + @Test + void createDynamicContent_createQrCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + + DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initiateCertificateChoice(); + Instant sessionResponseReceivedTime = Instant.now(); + + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); + String qrCodeDataUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.getSessionToken()) + .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withUserLanguage("eng") + .withAuthCode(authCode) + .createQrCodeDataUri(); + + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DynamicLinkType.QR_CODE, response.getSessionToken()); + } + + private static void assertUri(URI qrCodeUri, SessionType sessionType, DynamicLinkType dynamicLinkType, String sessionToken) { + assertEquals("https", qrCodeUri.getScheme()); + assertEquals("smart-id.com", qrCodeUri.getHost()); + assertEquals("/dynamic-link/", qrCodeUri.getPath()); + + assertTrue(qrCodeUri.getQuery().contains("version=0.1")); + assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("dynamicLinkType=" + dynamicLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); + assertTrue(qrCodeUri.getQuery().contains("elapsedSeconds=")); + assertTrue(qrCodeUri.getQuery().contains("lang=eng")); + assertTrue(qrCodeUri.getQuery().contains("authCode=")); + } } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 2c705c90..771183a6 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -61,16 +61,15 @@ import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SignatureAlgorithmParameters; class SmartIdRestConnectorTest { @@ -326,15 +325,15 @@ public void setUp() { @Test void getCertificate() { - stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v2/responses/dynamicLinkCertificateChoiceResponse.json"); + stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/responses/dynamic-link-certificate-choice-response.json"); CertificateRequest request = createCertificateRequest(); DynamicLinkCertificateChoiceSessionResponse response = connector.getCertificate(request); assertNotNull(response); - assertEquals("de305d54-75b4-431b-adb2-eb6b9e546016", response.getSessionID()); - assertEquals("session-token-value", response.getSessionToken()); - assertEquals("session-secret-value", response.getSessionSecret()); + assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + assertEquals("sampleSessionToken", response.getSessionToken()); + assertEquals("sampleSessionSecret", response.getSessionSecret()); } @Test @@ -566,7 +565,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); diff --git a/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json b/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json deleted file mode 100644 index 0f1fdbd8..00000000 --- a/src/test/resources/v2/responses/dynamicLinkCertificateChoiceResponse.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546016", - "sessionToken": "session-token-value", - "sessionSecret": "session-secret-value" -} diff --git a/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json b/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json deleted file mode 100644 index 85a818f0..00000000 --- a/src/test/resources/v2/responses/dynamicLinkSignatureResponse.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sessionID": "de305d54-75b4-431b-adb2-eb6b9e546016", - "sessionToken": "session-token-value", - "sessionSecret": "session-secret-value" -} \ No newline at end of file diff --git a/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json b/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json index a6cbb77c..7cb02a8c 100644 --- a/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json +++ b/src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json @@ -1,5 +1,5 @@ { - "sessionID": "abcdef1234567890", + "sessionID": "00000000-0000-0000-0000-000000000000", "sessionToken": "sampleSessionToken", "sessionSecret": "sampleSessionSecret" } \ No newline at end of file From 83bc26ddbb959e0004878a4dc503726ad7b6f3b0 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Sat, 30 Nov 2024 08:48:42 +0200 Subject: [PATCH 10/57] SLIB-48 - added notification based authentication request handling (#94) * SLIB-48 - added notification based authentication request handling * SLIB-48 - improved error messages, added javadoc-s, refatored tests * SLIB-48 - fix builder not using RP name and UUID provided in SmartIdClient --------- Co-authored-by: ragnar.haide --- README.md | 69 +++ ...java => AuthenticationSessionRequest.java} | 2 +- ...nkAuthenticationSessionRequestBuilder.java | 14 +- ...onAuthenticationSessionRequestBuilder.java | 358 ++++++++++++ ...ficationAuthenticationSessionResponse.java | 56 ++ .../java/ee/sk/smartid/v3/SmartIdClient.java | 11 + .../sk/smartid/v3/rest/SmartIdConnector.java | 27 +- .../smartid/v3/rest/SmartIdRestConnector.java | 60 +- ...thenticationSessionRequestBuilderTest.java | 56 +- ...thenticationSessionRequestBuilderTest.java | 539 ++++++++++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 40 +- .../v3/rest/SmartIdRestConnectorTest.java | 99 +++- .../v3/rest/SmartIdRestIntegrationTest.java | 12 +- ...cation-authentication-session-request.json | 15 + ...son => notification-session-response.json} | 0 15 files changed, 1296 insertions(+), 62 deletions(-) rename src/main/java/ee/sk/smartid/v3/{DynamicLinkAuthenticationSessionRequest.java => AuthenticationSessionRequest.java} (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java create mode 100644 src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java create mode 100644 src/test/resources/v3/requests/notification-authentication-session-request.json rename src/test/resources/v3/responses/{notification-signature-session-response.json => notification-session-response.json} (100%) diff --git a/README.md b/README.md index 04db32b1..7f0ceecb 100644 --- a/README.md +++ b/README.md @@ -1486,6 +1486,75 @@ SmartIdSignature signature = SmartIdSignature.fromSessionStatus(sessionStatus); // Use the signature as needed ``` +## Examples of performing notification authentication + +### Initiating notification authentication session with document number +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; + +String randomChallenge = RandomChallenge.generate(); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String verificationCode = authenticationSessionResponse.getVc().getValue(); +// Display the verification code to the user for confirmation +``` +After initiating the session, display the verificationCode to the user. The user must confirm that the code displayed in their Smart-ID app matches the one you have provided. + +### Initiating notification authentication session with semantics identifier +Alternatively, you can initiate a notification authentication session using a semantics identifier, which uniquely identifies the user across different countries and identity types. +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "30303039914" +); + +String randomChallenge = RandomChallenge.generate(); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String verificationCode = authenticationSessionResponse.getVc().getValue(); +// Display the verification code to the user for confirmation +``` + +### Requesting the IP Address of the User's Device +If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withSharedMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. +```java +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .withSharedMdClientIpAddress(true) // Request the user's device IP address + .initAuthenticationSession(); +``` + ### Generating QR-code or dynamic link Todo: will be implemented in task SLIB-55 ## Generating QR-code or dynamic link diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java rename to src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java index 28e26385..cdec0354 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java @@ -34,7 +34,7 @@ import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.RequestProperties; -public class DynamicLinkAuthenticationSessionRequest implements Serializable { +public class AuthenticationSessionRequest implements Serializable { private String relyingPartyUUID; diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java index 8df73d8e..8130ac86 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -75,6 +75,12 @@ public DynamicLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector this.connector = connector; } + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { this.relyingPartyUUID = relyingPartUUID; return this; @@ -210,13 +216,13 @@ public DynamicLinkAuthenticationSessionRequestBuilder withDocumentNumber(String */ public DynamicLinkAuthenticationSessionResponse initAuthenticationSession() { validateRequestParameters(); - DynamicLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); DynamicLinkAuthenticationSessionResponse dynamicLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(dynamicLinkAuthenticationSessionResponse); return dynamicLinkAuthenticationSessionResponse; } - private DynamicLinkAuthenticationSessionResponse initAuthenticationSession(DynamicLinkAuthenticationSessionRequest authenticationRequest) { + private DynamicLinkAuthenticationSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null) { return connector.initDynamicLinkAuthentication(authenticationRequest, semanticsIdentifier); } else if (documentNumber != null) { @@ -295,8 +301,8 @@ private void validateAllowedInteractionOrder() { allowedInteractionsOrder.forEach(Interaction::validate); } - private DynamicLinkAuthenticationSessionRequest createAuthenticationRequest() { - var request = new DynamicLinkAuthenticationSessionRequest(); + private AuthenticationSessionRequest createAuthenticationRequest() { + var request = new AuthenticationSessionRequest(); request.setRelyingPartyUUID(relyingPartyUUID); request.setRelyingPartyName(relyingPartyName); diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java new file mode 100644 index 00000000..cc614ee9 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java @@ -0,0 +1,358 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import java.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +/** + * Class for building a notification authentication session request + */ +public class NotificationAuthenticationSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationAuthenticationSessionRequestBuilder.class); + + private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = + Set.of(InteractionFlow.DISPLAY_TEXT_AND_PIN, InteractionFlow.CONFIRMATION_MESSAGE); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel; + private String randomChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; + private String nonce; + private List allowedInteractionsOrder; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + + /** + * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the random challenge + *

        + * The provided random challenge must be a Base64 encoded string + * + * @param randomChallenge the signature protocol parameters + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRandomChallenge(String randomChallenge) { + this.randomChallenge = randomChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the allowed interactions order + * + * @param allowedInteractionsOrder the allowed interactions order + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + this.allowedInteractionsOrder = allowedInteractionsOrder; + return this; + } + + /** + * Sets whether to share the Mobile-ID client IP address + * + * @param shareMdClientIpAddress whether to share the Mobile-ID client IP address + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSharedMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

        + * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

        + * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

        + * There are 2 supported ways to start authentication session: + *

          + *
        • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
        • + *
        • with document number by using {@link #withDocumentNumber(String)}
        • + *
        + * + * @return init session response + */ + public NotificationAuthenticationSessionResponse initAuthenticationSession() { + validateRequestParameters(); + AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(notificationAuthenticationSessionResponse); + return notificationAuthenticationSessionResponse; + } + + private NotificationAuthenticationSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null) { + return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initNotificationAuthentication(authenticationRequest, documentNumber); + } else { + throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set."); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + validateSignatureParameters(); + validateNonce(); + validateAllowedInteractionOrder(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(randomChallenge)) { + logger.error("Parameter randomChallenge must be set"); + throw new SmartIdClientException("Parameter randomChallenge must be set"); + } + byte[] challenge = getDecodedRandomChallenge(); + if (challenge.length < 32 || challenge.length > 64) { + logger.error("Size of parameter randomChallenge must be between 32 and 64 bytes"); + throw new SmartIdClientException("Size of parameter randomChallenge must be between 32 and 64 bytes"); + } + if (signatureAlgorithm == null) { + logger.error("Parameter signatureAlgorithm must be set"); + throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); + } + } + + private byte[] getDecodedRandomChallenge() { + Base64.Decoder decoder = Base64.getDecoder(); + try { + return decoder.decode(randomChallenge); + } catch (IllegalArgumentException e) { + logger.error("Parameter randomChallenge is not a valid Base64 encoded string"); + throw new SmartIdClientException("Parameter randomChallenge is not a valid Base64 encoded string"); + } + } + + private void validateNonce() { + if (nonce == null) { + return; + } + if (nonce.isEmpty()) { + logger.error("Parameter nonce value has to be at least 1 character long"); + throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); + } + if (nonce.length() > 30) { + logger.error("Nonce cannot be longer that 30 chars"); + throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); + } + } + + private void validateAllowedInteractionOrder() { + if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + logger.error("Parameter allowedInteractionsOrder must be set"); + throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); + } + Optional notSupportedInteraction = allowedInteractionsOrder.stream() + .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) + .findFirst(); + if (notSupportedInteraction.isPresent()) { + logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); + throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); + } + allowedInteractionsOrder.forEach(Interaction::validate); + } + + private AuthenticationSessionRequest createAuthenticationRequest() { + var request = new AuthenticationSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); + signatureProtocolParameters.setRandomChallenge(randomChallenge); + signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setNonce(nonce); + request.setAllowedInteractionsOrder(allowedInteractionsOrder); + + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + request.setRequestProperties(requestProperties); + } + request.setCapabilities(capabilities); + return request; + } + + private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { + if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + + VerificationCode verificationCode = notificationAuthenticationSessionResponse.getVc(); + if (verificationCode == null) { + logger.error("VC object is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC object is missing from the response"); + } + + String vcType = verificationCode.getType(); + if (StringUtil.isEmpty(vcType)) { + logger.error("VC type is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC type is missing from the response"); + } + + if (!VerificationCode.ALPHA_NUMERIC_4.equals(vcType)) { + logger.error("Unsupported VC type: {}", vcType); + throw new UnprocessableSmartIdResponseException("Unsupported VC type: " + vcType); + } + + if (StringUtil.isEmpty(verificationCode.getValue())) { + logger.error("VC value is missing from the response"); + throw new UnprocessableSmartIdResponseException("VC value is missing from the response"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java new file mode 100644 index 00000000..428bf95e --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java @@ -0,0 +1,56 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class NotificationAuthenticationSessionResponse implements Serializable { + + private String sessionID; + + private VerificationCode vc; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + public VerificationCode getVc() { + return vc; + } + + public void setVc(VerificationCode verificationCode) { + this.vc = verificationCode; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index b09eed6e..e085aabd 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -84,6 +84,17 @@ public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentic return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()); } + /** + * Creates a new builder for creating a new notification authentication session request + * + * @return builder for creating a new notification authentication session request + */ + public NotificationAuthenticationSessionRequestBuilder createNotificationAuthentication() { + return new NotificationAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + /** * Creates a new builder for creating a new dynamic link signature session request * diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index 62c85a44..35124e41 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,8 +32,9 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.AuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; @@ -118,7 +119,7 @@ public interface SmartIdConnector extends Serializable { * @param authenticationRequest The dynamic link authentication session request * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest); + DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest); /** * Create authentication session with dynamic link using semantics identifier @@ -127,7 +128,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); /** * Create authentication session with dynamic link using document number @@ -136,5 +137,23 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); + DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); + + /** + * Create authentication session with notification using semantics identifier + * + * @param authenticationRequest The notification authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with notification using document number + * + * @param authenticationRequest The notification authentication session request + * @param documentNumber The document number + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index fe4627cb..452803b5 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -45,11 +45,12 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.LoggingFilter; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.AuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; +import ee.sk.smartid.v3.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.CertificateRequest; import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -88,6 +89,9 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/dynamic-link/document"; + private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; + private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; + private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; @@ -123,32 +127,52 @@ public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundEx } @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { logger.debug("Starting dynamic link authentication session with semantics identifier"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postAuthenticationRequest(uri, authenticationRequest); + return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); } @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { + public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { logger.debug("Starting dynamic link authentication session with document number"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postAuthenticationRequest(uri, authenticationRequest); + return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); } @Override - public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(DynamicLinkAuthenticationSessionRequest authenticationRequest) { + public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { logger.debug("Starting anonymous dynamic link authentication session"); URI uri = UriBuilder.fromUri(endpointUrl) .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) .build(); - return postAuthenticationRequest(uri, authenticationRequest); + return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postNotificationAuthenticationRequest(uri, authenticationRequest); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postNotificationAuthenticationRequest(uri, authenticationRequest); } @Override @@ -169,7 +193,7 @@ public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSes .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postSignatureRequest(uri, request); + return postDynamicLinkSignatureRequest(uri, request); } @Override @@ -179,7 +203,7 @@ public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSes .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postSignatureRequest(uri, request); + return postDynamicLinkSignatureRequest(uri, request); } public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { @@ -251,7 +275,7 @@ protected String getJdkMajorVersion() { } } - private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI uri, DynamicLinkAuthenticationSessionRequest request) { + private DynamicLinkAuthenticationSessionResponse postDynamicLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { try { return postRequest(uri, request, DynamicLinkAuthenticationSessionResponse.class); } catch (NotFoundException e) { @@ -263,6 +287,18 @@ private DynamicLinkAuthenticationSessionResponse postAuthenticationRequest(URI u } } + private NotificationAuthenticationSessionResponse postNotificationAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { + try { + return postRequest(uri, request, NotificationAuthenticationSessionResponse.class); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException e) { + logger.warn("No permission to issue the request", e); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); + } + } + private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI uri, CertificateRequest request) { try { return postRequest(uri, request, DynamicLinkCertificateChoiceSessionResponse.class); @@ -275,7 +311,7 @@ private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI u } } - private DynamicLinkSignatureSessionResponse postSignatureRequest(URI uri, SignatureSessionRequest request) { + private DynamicLinkSignatureSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { return postRequest(uri, request, DynamicLinkSignatureSessionResponse.class); } catch (NotFoundException ex) { diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java index d975b1eb..6f06b28a 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -75,7 +75,7 @@ class ValidateRequiredRequestParameters { @Test public void initAuthenticationSession_ok() { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -84,9 +84,9 @@ public void initAuthenticationSession_ok() { .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); assertEquals("DEMO", request.getRelyingPartyName()); @@ -101,7 +101,7 @@ public void initAuthenticationSession_ok() { @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -112,9 +112,9 @@ public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertific .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.getCertificateLevel()); } @@ -122,7 +122,7 @@ public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertific @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) public void initAuthenticationSession_nonce_ok(String nonce) { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -133,9 +133,9 @@ public void initAuthenticationSession_nonce_ok(String nonce) { .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.getNonce()); } @@ -143,7 +143,7 @@ public void initAuthenticationSession_nonce_ok(String nonce) { @ParameterizedTest @EnumSource public void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -154,16 +154,16 @@ public void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm s .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); } @Test public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -173,9 +173,9 @@ public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestPrope .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertNull(request.getRequestProperties()); } @@ -183,7 +183,7 @@ public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestPrope @ParameterizedTest @ValueSource(booleans = {true, false}) public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))) + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -194,9 +194,9 @@ public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) .withSharedMdClientIpAddress(ipRequested) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertNotNull(request.getRequestProperties()); assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); @@ -205,7 +205,7 @@ public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) public void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -215,9 +215,9 @@ public void initAuthenticationSession_capabilities_ok(String[] capabilities, Set .withCapabilities(capabilities) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DynamicLinkAuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - DynamicLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + AuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(expectedCapabilities, request.getCapabilities()); } @@ -348,7 +348,7 @@ public void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwEx var exception = assertThrows(SmartIdClientException.class, () -> { var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); initAuthentication(); }); @@ -362,7 +362,7 @@ public void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_thro var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionResponse.setSessionToken(sessionToken); - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); initAuthentication(); }); @@ -377,7 +377,7 @@ public void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_thr dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); - when(connector.initAnonymousDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); initAuthentication(); }); @@ -396,7 +396,7 @@ private void initAuthentication() { @Test void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -408,7 +408,7 @@ void initAuthenticationSession_withSemanticsIdentifier() { .initAuthenticationSession(); ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + verify(connector).initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); @@ -416,7 +416,7 @@ void initAuthenticationSession_withSemanticsIdentifier() { @Test void initAuthenticationSession_withDocumentNumber() { - when(connector.initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), any(String.class))) + when(connector.initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), any(String.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -428,7 +428,7 @@ void initAuthenticationSession_withDocumentNumber() { .initAuthenticationSession(); ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initDynamicLinkAuthentication(any(DynamicLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); + verify(connector).initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), documentNumberCaptor.capture()); String capturedDocumentNumber = documentNumberCaptor.getValue(); assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); diff --git a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java new file mode 100644 index 00000000..09c137be --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java @@ -0,0 +1,539 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.Interaction; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.VerificationCode; + +class NotificationAuthenticationSessionRequestBuilderTest { + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + void initAuthenticationSession_ok() { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); + assertEquals("DEMO", request.getRelyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V1, request.getSignatureProtocol()); + assertNotNull(request.getSignatureProtocolParameters()); + assertEquals("sha512WithRSAEncryption", request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertNotNull(request.getAllowedInteractionsOrder()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withSignatureAlgorithm(signatureAlgorithm) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); + } + + @Test + void initAuthenticationSession_withNonce() { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withNonce("uniqueNonce") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("uniqueNonce", request.getNonce()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withSharedMdClientIpAddress(ipRequested) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.getRequestProperties()); + assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withCapabilities(capabilities) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.getCapabilities()); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); + + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName("DEMO") + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName(relyingPartyName) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals("Parameter randomChallenge must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRandomChallengeArgumentProvider.class) + void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withSignatureAlgorithm(null) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidNonceProvider.class) + void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withNonce(invalidNonce) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(interactions) + .initAuthenticationSession()); + assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(NotSupportedInteractionsProvider.class) + void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(interaction)) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInteractionsProvider.class) + void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(interaction)) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession()); + + assertEquals("Either documentNumber or semanticsIdentifier must be set.", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); + notificationAuthenticationSessionResponse.setSessionID(sessionId); + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + }); + assertEquals("Session ID is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullSource + void initAuthenticationSession_vcIsNotPresentInTheResponse_throwException(VerificationCode vc) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); + notificationAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + notificationAuthenticationSessionResponse.setVc(vc); + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession(); + }); + assertEquals("VC object is missing from the response", exception.getMessage()); + } + + @Test + void initAuthenticationSession_missingVcType_throwException() { + var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse(null, "4927"); + + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession()); + + assertEquals("VC type is missing from the response", exception.getMessage()); + } + + @Test + void initAuthenticationSession_unsupportedVcType_throwException() { + var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse("numeric8", "4927"); + + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession()); + + assertEquals("Unsupported VC type: numeric8", exception.getMessage()); + } + + @Test + void initAuthenticationSession_missingVcValue_throwException() { + var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse("alphaNumeric4", null); + + when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> + new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(generateBase64String("a".repeat(32))) + .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initAuthenticationSession()); + + assertEquals("VC value is missing from the response", exception.getMessage()); + } + } + + private NotificationAuthenticationSessionResponse createNotificationAuthenticationResponse(String vcType, String vcValue) { + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); + notificationAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + + var verificationCode = new VerificationCode(); + verificationCode.setType(vcType); + verificationCode.setValue(vcValue); + + notificationAuthenticationSessionResponse.setVc(verificationCode); + return notificationAuthenticationSessionResponse; + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[0], Collections.emptySet()), + Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), + Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) + ); + } + } + + private static class InvalidRandomChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Parameter randomChallenge is not a valid Base64 encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(31).getBytes())), + "Size of parameter randomChallenge must be between 32 and 64 bytes"), + Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(65).getBytes())), + "Size of parameter randomChallenge must be between 32 and 64 bytes") + ); + } + } + + private static class InvalidNonceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") + ); + } + } + + private static class NotSupportedInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Interaction.displayTextAndPIN("PIN code display"), "AllowedInteractionsOrder contains not supported interaction DISPLAY_TEXT_AND_PIN"), + Arguments.of(Interaction.confirmationMessage("Confirmation message"), "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE") + ); + } + } + + private static class InvalidInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided text is null", Interaction.verificationCodeChoice(null)), + "displayText60 cannot be null for AllowedInteractionOrder of type VERIFICATION_CODE_CHOICE"), + Arguments.of(Named.of("provided text is longer than allowed 60", Interaction.verificationCodeChoice("a".repeat(61))), + "displayText60 must not be longer than 60 characters"), + Arguments.of(Named.of("provided text is null", Interaction.confirmationMessageAndVerificationCodeChoice(null)), + "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE"), + Arguments.of(Named.of("provided text is longer than allowed 200", Interaction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))), + "displayText200 must not be longer than 200 characters") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 19986118..c9e3762c 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -192,13 +192,49 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { } } + @Nested + @WireMockTest(httpPort = 18089) + class NotificationAuthenticationSession { + + @Test + void createNotificationAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + + @Test + void createNotificationAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .initAuthenticationSession(); + + assertNotNull(response.getSessionID()); + assertNotNull(response.getVc()); + assertNotNull(response.getVc().getType()); + assertNotNull(response.getVc().getValue()); + } + } @Nested @WireMockTest(httpPort = 18089) class NotificationBasedSignatureSession { @Test void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); @@ -221,7 +257,7 @@ void createNotificationSignature_withDocumentNumber() { @Test void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 771183a6..2e9a73bc 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -59,9 +59,10 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.AuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.SignatureSessionRequest; @@ -312,6 +313,78 @@ void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException } } + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierNotificationAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initNotificationAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + + @Test + void initNotificationAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberNotificationAuthentication { + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initNotificationAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + + @Test + void initNotificationAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + } + @Nested @WireMockTest(httpPort = 18089) class CertificateChoiceTests { @@ -565,7 +638,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -668,7 +741,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -757,8 +830,8 @@ void initNotificationSignature_throwsServerMaintenanceException() { } } - private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { - var dynamicLinkAuthenticationSessionRequest = new DynamicLinkAuthenticationSessionRequest(); + private AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); @@ -773,6 +846,22 @@ private DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessi return dynamicLinkAuthenticationSessionRequest; } + private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { + var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); + dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); + + var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); + signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); + signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); + + Interaction interaction = Interaction.verificationCodeChoice("Verify the code"); + dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); + + return dynamicLinkAuthenticationSessionRequest; + } + private CertificateRequest createCertificateRequest() { var request = new CertificateRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java index 2fd6b8d1..724eb626 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java @@ -34,7 +34,7 @@ import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionRequest; +import ee.sk.smartid.v3.AuthenticationSessionRequest; import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; import ee.sk.smartid.v3.RandomChallenge; import ee.sk.smartid.v3.SignatureAlgorithm; @@ -54,7 +54,7 @@ void setUp() { @Test void authenticate_anonymous() { - DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); @@ -63,7 +63,7 @@ void authenticate_anonymous() { @Test void authenticate_withDocumentNumber() { - DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); @@ -72,15 +72,15 @@ void authenticate_withDocumentNumber() { @Test void authenticate_withSemanticsIdentifier() { - DynamicLinkAuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); } - private static DynamicLinkAuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { - DynamicLinkAuthenticationSessionRequest request = new DynamicLinkAuthenticationSessionRequest(); + private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + AuthenticationSessionRequest request = new AuthenticationSessionRequest(); request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); request.setRelyingPartyName("DEMO"); request.setCertificateLevel("QUALIFIED"); diff --git a/src/test/resources/v3/requests/notification-authentication-session-request.json b/src/test/resources/v3/requests/notification-authentication-session-request.json new file mode 100644 index 00000000..c110d789 --- /dev/null +++ b/src/test/resources/v3/requests/notification-authentication-session-request.json @@ -0,0 +1,15 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V1", + "signatureProtocolParameters": { + "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "sha512WithRSAEncryption" + }, + "allowedInteractionsOrder": [ + { + "type": "verificationCodeChoice", + "displayText60": "Verify the code" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/v3/responses/notification-signature-session-response.json b/src/test/resources/v3/responses/notification-session-response.json similarity index 100% rename from src/test/resources/v3/responses/notification-signature-session-response.json rename to src/test/resources/v3/responses/notification-session-response.json From 2588206f23911d9220648f93fc203fdf17e4ba38 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Sat, 7 Dec 2024 07:13:17 +0200 Subject: [PATCH 11/57] SLIB-67 - refactoring dynamic link based auth flow (#95) * SLIB-67 - refactoring dynamic link based auth flow * SLIB-67 - removed unnecessary abstract classes, refactored * SLIB-67 - added tests, relocated classes * SLIB-67 - added tests * SLIB-67 - added test --------- Co-authored-by: ragnar.haide --- README.md | 47 ++---- ...nkAuthenticationSessionRequestBuilder.java | 13 +- ...amicLinkAuthenticationSessionResponse.java | 58 ------- ...ertificateChoiceSessionRequestBuilder.java | 27 ++-- ...e.java => DynamicLinkSessionResponse.java} | 2 +- ...micLinkSignatureSessionRequestBuilder.java | 63 ++------ ...onAuthenticationSessionRequestBuilder.java | 1 + ...icationSignatureSessionRequestBuilder.java | 43 +---- .../java/ee/sk/smartid/v3/SignatureUtil.java | 66 ++++++++ .../java/ee/sk/smartid/v3/SmartIdClient.java | 1 - .../SmartIdRequestBuilderService.java | 7 +- .../sk/smartid/v3/rest/SmartIdConnector.java | 22 ++- .../smartid/v3/rest/SmartIdRestConnector.java | 36 ++--- .../dao}/AuthenticationSessionRequest.java | 10 +- ...a => CertificateChoiceSessionRequest.java} | 2 +- ...cLinkCertificateChoiceSessionResponse.java | 60 ------- .../dao}/SignatureSessionRequest.java | 6 +- ...thenticationSessionRequestBuilderTest.java | 51 +++--- ...ficateChoiceSessionRequestBuilderTest.java | 100 +++++------- ...inkSignatureSessionRequestBuilderTest.java | 39 ++--- ...thenticationSessionRequestBuilderTest.java | 1 + ...ionSignatureSessionRequestBuilderTest.java | 1 + .../ee/sk/smartid/v3/SignatureUtilTest.java | 147 ++++++++++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 119 +++++++------- .../SmartIdRequestBuilderServiceTest.java | 6 +- .../v3/rest/SmartIdRestConnectorTest.java | 46 +++--- .../v3/rest/SmartIdRestIntegrationTest.java | 14 +- ...namic-link-certificate-choice-request.json | 5 +- .../dynamic-link-signature-request.json | 5 +- 29 files changed, 488 insertions(+), 510 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java rename src/main/java/ee/sk/smartid/v3/{service => }/DynamicLinkCertificateChoiceSessionRequestBuilder.java (88%) rename src/main/java/ee/sk/smartid/v3/{DynamicLinkSignatureSessionResponse.java => DynamicLinkSessionResponse.java} (96%) create mode 100644 src/main/java/ee/sk/smartid/v3/SignatureUtil.java rename src/main/java/ee/sk/smartid/v3/{service => }/SmartIdRequestBuilderService.java (98%) rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/AuthenticationSessionRequest.java (96%) rename src/main/java/ee/sk/smartid/v3/rest/dao/{CertificateRequest.java => CertificateChoiceSessionRequest.java} (97%) delete mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/SignatureSessionRequest.java (96%) rename src/test/java/ee/sk/smartid/v3/{service => }/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java (59%) create mode 100644 src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java rename src/test/java/ee/sk/smartid/v3/{service => }/SmartIdRequestBuilderServiceTest.java (99%) diff --git a/README.md b/README.md index 7f0ceecb..90e7db88 100644 --- a/README.md +++ b/README.md @@ -903,7 +903,7 @@ SmartIdClient client=new SmartIdClient(); client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() +DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() .withRelyingPartyUUID(client.getRelyingPartyUUID()) .withRelyingPartyName(client.getRelyingPartyName()) .withCertificateLevel("QUALIFIED") @@ -921,29 +921,6 @@ The response from a successful dynamic link certificate choice session creation * `sessionToken`: Unique random value that will be used to connect this certificate choice attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -## Fetching Session Status -After initiating the dynamic link certificate choice session and storing the session information, you can fetch the session status to check if the user has completed the authentication process. - -```java -// Fetch the final session status -SessionStatusPoller poller = client.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - -// Validate the session status -var requestBuilder = new SmartIdRequestBuilderService(); -requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", null, null); - -// Create authentication response -SmartIdAuthenticationResponse authenticationResponse = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, null); - -// Extract user information -AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(authenticationResponse.getCertificate()); -String givenName = identity.getGivenName(); -String surname = identity.getSurname(); -String identityCode = identity.getIdentityCode(); -String country = identity.getCountry(); -``` - ## Validating Parameters Ensure that you validate the parameters before initiating the request. For example, the `nonce` must be between 1 and 30 characters. @@ -952,7 +929,7 @@ Handle exceptions appropriately. The Java client provides specific exceptions fo ```java try { - CertificateChoiceResponse response = builder.initiateCertificateChoice(); + CertificateChoiceResponse response = builder.initCertificateChoice(); // Proceed with session status fetching and validation } catch (UserAccountNotFoundException e) { System.out.println("User account not found."); @@ -980,13 +957,13 @@ client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -DynamicLinkCertificateChoiceSessionResponse response = client.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withShareMdClientIpAddress(true) - .initiateCertificateChoice(); + DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withShareMdClientIpAddress(true) + .initiateCertificateChoice(); ``` # Initiating a Dynamic Link Signature Session in API v3.0 @@ -1086,7 +1063,7 @@ var builder = client.createDynamicLinkSignature() .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); // Initiate the dynamic link signature -DynamicLinkSignatureSessionResponse signatureResponse = builder.initSignatureSession(); +DynamicLinkSessionResponse signatureResponse = builder.initSignatureSession(); // Process the signature response String sessionID = signatureResponse.getSessionID(); @@ -1120,7 +1097,7 @@ var builder = client.createDynamicLinkSignature() .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); // Initiate the dynamic link signature -DynamicLinkSignatureSessionResponse signatureResponse = builder.initSignatureSession(); +DynamicLinkSessionResponse signatureResponse = builder.initSignatureSession(); // Process the signature response String sessionID = signatureResponse.getSessionID(); @@ -1142,7 +1119,7 @@ Handle exceptions appropriately. The Java client provides specific exceptions fo ```java try { -DynamicLinkSignatureSessionResponse response = builder.initSignatureSession(); +DynamicLinkSessionResponse response = builder.initSignatureSession(); String sessionID = response.getSessionID(); String sessionToken = response.getSessionToken(); diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java index 8130ac86..e7af37d3 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -37,6 +37,7 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.InteractionFlow; import ee.sk.smartid.v3.rest.dao.RequestProperties; @@ -214,15 +215,15 @@ public DynamicLinkAuthenticationSessionRequestBuilder withDocumentNumber(String * * @return init session response */ - public DynamicLinkAuthenticationSessionResponse initAuthenticationSession() { + public DynamicLinkSessionResponse initAuthenticationSession() { validateRequestParameters(); AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - DynamicLinkAuthenticationSessionResponse dynamicLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + DynamicLinkSessionResponse dynamicLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(dynamicLinkAuthenticationSessionResponse); return dynamicLinkAuthenticationSessionResponse; } - private DynamicLinkAuthenticationSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { + private DynamicLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null) { return connector.initDynamicLinkAuthentication(authenticationRequest, semanticsIdentifier); } else if (documentNumber != null) { @@ -326,7 +327,7 @@ private AuthenticationSessionRequest createAuthenticationRequest() { return request; } - private void validateResponseParameters(DynamicLinkAuthenticationSessionResponse dynamicLinkAuthenticationSessionResponse) { + private void validateResponseParameters(DynamicLinkSessionResponse dynamicLinkAuthenticationSessionResponse) { if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionID())) { logger.error("Session ID is missing from the response"); throw new SmartIdClientException("Session ID is missing from the response"); diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java deleted file mode 100644 index 2180dba3..00000000 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -package ee.sk.smartid.v3; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -public class DynamicLinkAuthenticationSessionResponse { - - private String sessionID; - private String sessionToken; - private String sessionSecret; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } - - public String getSessionToken() { - return sessionToken; - } - - public void setSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - } - - public String getSessionSecret() { - return sessionSecret; - } - - public void setSessionSecret(String sessionSecret) { - this.sessionSecret = sessionSecret; - } -} diff --git a/src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java similarity index 88% rename from src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java index c34f1119..95213065 100644 --- a/src/main/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.service; +package ee.sk.smartid.v3; /*- * #%L @@ -34,10 +34,8 @@ import org.slf4j.LoggerFactory; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v3.CertificateLevel; import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.v3.rest.dao.RequestProperties; public class DynamicLinkCertificateChoiceSessionRequestBuilder { @@ -50,7 +48,7 @@ public class DynamicLinkCertificateChoiceSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private boolean shareMdClientIpAddress; + private Boolean shareMdClientIpAddress; /** * Constructs a new DynamicLinkCertificateRequestBuilder with the given Smart-ID connector @@ -111,8 +109,9 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) * @param capabilities the capabilities * @return this builder */ - public void withCapabilities(Set capabilities) { - this.capabilities = capabilities; + public DynamicLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; } /** @@ -134,10 +133,10 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddr * @return DynamicLinkCertificateChoiceSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. * @throws SmartIdClientException if the response is invalid or missing necessary session data. */ - public DynamicLinkCertificateChoiceSessionResponse initiateCertificateChoice() { + public DynamicLinkSessionResponse initCertificateChoice() { validateParameters(); - CertificateRequest request = createCertificateRequest(); - DynamicLinkCertificateChoiceSessionResponse response = connector.getCertificate(request); + CertificateChoiceSessionRequest request = createCertificateRequest(); + DynamicLinkSessionResponse response = connector.getCertificate(request); if (response == null || response.getSessionID() == null) { throw new SmartIdClientException("Dynamic link certificate choice session failed: invalid response received."); @@ -155,12 +154,12 @@ private void validateParameters() { throw new SmartIdClientException("Parameter relyingPartyName must be set"); } if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { - throw new SmartIdClientException("Nonce must be between 1 and 30 characters. You supplied: '" + nonce + "'"); + throw new SmartIdClientException("Nonce must be between 1 and 30 characters"); } } - private CertificateRequest createCertificateRequest() { - var request = new CertificateRequest(); + private CertificateChoiceSessionRequest createCertificateRequest() { + var request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID(relyingPartyUUID); request.setRelyingPartyName(relyingPartyName); @@ -179,4 +178,4 @@ private CertificateRequest createCertificateRequest() { return request; } -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java rename to src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java index 0a18b796..0d64b19e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java @@ -31,7 +31,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public class DynamicLinkSignatureSessionResponse implements Serializable { +public class DynamicLinkSessionResponse implements Serializable { private String sessionID; private String sessionToken; diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java index 44a66e02..6c17f9e8 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; @@ -42,6 +41,7 @@ import ee.sk.smartid.v3.rest.dao.InteractionFlow; import ee.sk.smartid.v3.rest.dao.RequestProperties; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; public class DynamicLinkSignatureSessionRequestBuilder { @@ -60,7 +60,7 @@ public class DynamicLinkSignatureSessionRequestBuilder { private String nonce; private Set capabilities; private List allowedInteractionsOrder; - private boolean shareMdClientIpAddress; + private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private SignableData signableData; private SignableHash signableHash; @@ -147,8 +147,8 @@ public DynamicLinkSignatureSessionRequestBuilder withNonce(String nonce) { * @param capabilities the capabilities * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withCapabilities(Set capabilities) { - this.capabilities = capabilities; + public DynamicLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); return this; } @@ -236,18 +236,18 @@ public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boole *
      • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
      • *
      * - * @return a {@link DynamicLinkSignatureSessionResponse} containing session details such as + * @return a {@link DynamicLinkSessionResponse} containing session details such as * session ID, session token, and session secret. */ - public DynamicLinkSignatureSessionResponse initSignatureSession() { + public DynamicLinkSessionResponse initSignatureSession() { validateParameters(); SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - DynamicLinkSignatureSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + DynamicLinkSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); validateResponseParameters(dynamicLinkSignatureSessionResponse); return dynamicLinkSignatureSessionResponse; } - private DynamicLinkSignatureSessionResponse initSignatureSession(SignatureSessionRequest request) { + private DynamicLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { if (documentNumber != null) { return connector.initDynamicLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { @@ -268,55 +268,22 @@ private SignatureSessionRequest createSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); if (signableHash != null || signableData != null) { - signatureProtocolParameters.setDigest(getDigestToSignBase64()); + signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); } - signatureProtocolParameters.setSignatureAlgorithm(getSignatureAlgorithm()); + signatureProtocolParameters.setSignatureAlgorithm(SignatureUtil.getSignatureAlgorithm(signatureAlgorithm, signableHash, signableData)); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setNonce(nonce); request.setAllowedInteractionsOrder(allowedInteractionsOrder); - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - if (requestProperties.hasProperties()) { + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } request.setCapabilities(capabilities); return request; } - private String getDigestToSignBase64() { - if (signableHash != null && signableHash.areFieldsFilled()) { - return signableHash.getHashInBase64(); - } else if (signableData != null) { - if (signableData.getHashType() == null) { - throw new SmartIdClientException("HashType must be set for signableData."); - } - return signableData.calculateHashInBase64(); - } else { - throw new SmartIdClientException("Either signableHash or signableData must be set."); - } - } - - private String getSignatureAlgorithm() { - if (signatureAlgorithm != null) { - return signatureAlgorithm.getAlgorithmName(); - } else if (signableHash != null && signableHash.getHashType() != null) { - return getSignatureAlgorithmName(signableHash.getHashType()); - } else if (signableData != null && signableData.getHashType() != null) { - return getSignatureAlgorithmName(signableData.getHashType()); - } else { - return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - } - } - - private String getSignatureAlgorithmName(HashType hashType) { - return switch (hashType) { - case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); - case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); - case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - }; - } - private void validateParameters() { if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { throw new SmartIdClientException("Relying Party UUID must be set."); @@ -348,7 +315,7 @@ private void validateAllowedInteractions() { allowedInteractionsOrder.forEach(Interaction::validate); } - private void validateResponseParameters(DynamicLinkSignatureSessionResponse dynamicLinkSignatureSessionResponse) { + private void validateResponseParameters(DynamicLinkSessionResponse dynamicLinkSignatureSessionResponse) { if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionID())) { logger.error("Session ID is missing from the response"); throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); @@ -364,4 +331,4 @@ private void validateResponseParameters(DynamicLinkSignatureSessionResponse dyna throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); } } -} +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java index cc614ee9..f27a5712 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java @@ -38,6 +38,7 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.InteractionFlow; import ee.sk.smartid.v3.rest.dao.RequestProperties; diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java index 8d10d9c4..38a8c0cd 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; @@ -42,6 +41,7 @@ import ee.sk.smartid.v3.rest.dao.InteractionFlow; import ee.sk.smartid.v3.rest.dao.RequestProperties; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.VerificationCode; public class NotificationSignatureSessionRequestBuilder { @@ -61,7 +61,7 @@ public class NotificationSignatureSessionRequestBuilder { private String nonce; private Set capabilities; private List allowedInteractionsOrder; - private boolean shareMdClientIpAddress; + private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private SignableData signableData; private SignableHash signableHash; @@ -254,14 +254,14 @@ private SignatureSessionRequest createSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); if (signableHash != null || signableData != null) { - signatureProtocolParameters.setDigest(getDigestToSignBase64()); + signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); } - signatureProtocolParameters.setSignatureAlgorithm(getSignatureAlgorithm()); + signatureProtocolParameters.setSignatureAlgorithm(SignatureUtil.getSignatureAlgorithm(signatureAlgorithm, signableHash, signableData)); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setNonce(nonce); request.setAllowedInteractionsOrder(allowedInteractionsOrder); - if (this.shareMdClientIpAddress) { + if (this.shareMdClientIpAddress != null) { var requestProperties = new RequestProperties(); requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); @@ -271,39 +271,6 @@ private SignatureSessionRequest createSignatureSessionRequest() { return request; } - private String getDigestToSignBase64() { - if (signableHash != null && signableHash.areFieldsFilled()) { - return signableHash.getHashInBase64(); - } else if (signableData != null) { - if (signableData.getHashType() == null) { - throw new SmartIdClientException("HashType must be set for signableData."); - } - return signableData.calculateHashInBase64(); - } else { - throw new SmartIdClientException("Either signableHash or signableData must be set."); - } - } - - private String getSignatureAlgorithm() { - if (signatureAlgorithm != null) { - return signatureAlgorithm.getAlgorithmName(); - } else if (signableHash != null && signableHash.getHashType() != null) { - return getSignatureAlgorithmName(signableHash.getHashType()); - } else if (signableData != null && signableData.getHashType() != null) { - return getSignatureAlgorithmName(signableData.getHashType()); - } else { - return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - } - } - - private String getSignatureAlgorithmName(HashType hashType) { - return switch (hashType) { - case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); - case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); - case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - }; - } - private void validateParameters() { if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { throw new SmartIdClientException("Relying Party UUID must be set."); diff --git a/src/main/java/ee/sk/smartid/v3/SignatureUtil.java b/src/main/java/ee/sk/smartid/v3/SignatureUtil.java new file mode 100644 index 00000000..c4bc3350 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignatureUtil.java @@ -0,0 +1,66 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class SignatureUtil { + + public static String getDigestToSignBase64(SignableHash signableHash, SignableData signableData) { + if (signableHash != null && signableHash.areFieldsFilled()) { + return signableHash.getHashInBase64(); + } else if (signableData != null) { + if (signableData.getHashType() == null) { + throw new SmartIdClientException("HashType must be set for signableData."); + } + return signableData.calculateHashInBase64(); + } else { + throw new SmartIdClientException("Either signableHash or signableData must be set."); + } + } + + public static String getSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm, SignableHash signableHash, SignableData signableData) { + if (signatureAlgorithm != null) { + return signatureAlgorithm.getAlgorithmName(); + } else if (signableHash != null && signableHash.getHashType() != null) { + return getAlgorithmFromHashType(signableHash.getHashType()); + } else if (signableData != null && signableData.getHashType() != null) { + return getAlgorithmFromHashType(signableData.getHashType()); + } else { + return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + } + } + + private static String getAlgorithmFromHashType(HashType hashType) { + return switch (hashType) { + case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); + case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); + case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index e085aabd..859d8d6a 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -47,7 +47,6 @@ import ee.sk.smartid.v3.rest.SessionStatusPoller; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.SmartIdRestConnector; -import ee.sk.smartid.v3.service.DynamicLinkCertificateChoiceSessionRequestBuilder; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.Configuration; diff --git a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java b/src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java rename to src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java index a22255ca..2cddbdb5 100644 --- a/src/main/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderService.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.service; +package ee.sk.smartid.v3; /*- * #%L @@ -54,11 +54,6 @@ import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.CertificateLevel; -import ee.sk.smartid.v3.SignableData; -import ee.sk.smartid.v3.SignableHash; -import ee.sk.smartid.v3.SignatureProtocol; -import ee.sk.smartid.v3.SmartIdAuthenticationResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionCertificate; diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index 35124e41..bdad790c 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,14 +32,12 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v3.AuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.SignatureSessionRequest; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.CertificateRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; @@ -68,7 +66,7 @@ public interface SmartIdConnector extends Serializable { * @param request CertificateRequest containing necessary parameters * @return CertificateChoiceResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateRequest request); + DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest request); /** * Initiates a dynamic link based signature sessions. @@ -77,7 +75,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a dynamic link based signature sessions. @@ -86,7 +84,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); + DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); /** * Initiates a notification-based signature session using a semantics identifier. @@ -119,7 +117,7 @@ public interface SmartIdConnector extends Serializable { * @param authenticationRequest The dynamic link authentication session request * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest); + DynamicLinkSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest); /** * Create authentication session with dynamic link using semantics identifier @@ -128,7 +126,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); /** * Create authentication session with dynamic link using document number @@ -137,7 +135,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return The dynamic link authentication session response */ - DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); + DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); /** * Create authentication session with notification using semantics identifier diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index 452803b5..fd4f6552 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -45,14 +45,12 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.LoggingFilter; -import ee.sk.smartid.v3.AuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.SignatureSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; @@ -127,7 +125,7 @@ public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundEx } @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + public DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { logger.debug("Starting dynamic link authentication session with semantics identifier"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -137,7 +135,7 @@ public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(Au } @Override - public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { + public DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { logger.debug("Starting dynamic link authentication session with document number"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) @@ -147,7 +145,7 @@ public DynamicLinkAuthenticationSessionResponse initDynamicLinkAuthentication(Au } @Override - public DynamicLinkAuthenticationSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { + public DynamicLinkSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { logger.debug("Starting anonymous dynamic link authentication session"); URI uri = UriBuilder.fromUri(endpointUrl) .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) @@ -176,18 +174,18 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public DynamicLinkCertificateChoiceSessionResponse getCertificate(CertificateRequest request) { + public DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest request) { logger.debug("Initiating dynamic link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) .path(CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH) .build(); - return postCertificateRequest(uri, request); + return postCertificateChoiceRequest(uri, request); } @Override - public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -197,7 +195,7 @@ public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSes } @Override - public DynamicLinkSignatureSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { + public DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) @@ -275,9 +273,9 @@ protected String getJdkMajorVersion() { } } - private DynamicLinkAuthenticationSessionResponse postDynamicLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { + private DynamicLinkSessionResponse postDynamicLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkAuthenticationSessionResponse.class); + return postRequest(uri, request, DynamicLinkSessionResponse.class); } catch (NotFoundException e) { logger.warn("User account not found for URI " + uri, e); throw new UserAccountNotFoundException(); @@ -299,9 +297,9 @@ private NotificationAuthenticationSessionResponse postNotificationAuthentication } } - private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI uri, CertificateRequest request) { + private DynamicLinkSessionResponse postCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkCertificateChoiceSessionResponse.class); + return postRequest(uri, request, DynamicLinkSessionResponse.class); } catch (NotFoundException ex) { logger.warn("User account not found for URI {}", uri, ex); throw new UserAccountNotFoundException(); @@ -311,9 +309,9 @@ private DynamicLinkCertificateChoiceSessionResponse postCertificateRequest(URI u } } - private DynamicLinkSignatureSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { + private DynamicLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkSignatureSessionResponse.class); + return postRequest(uri, request, DynamicLinkSessionResponse.class); } catch (NotFoundException ex) { logger.warn("User account not found for URI " + uri, ex); throw new UserAccountNotFoundException(); diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java index cdec0354..db3b75e1 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,6 +31,8 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.v3.SignatureProtocol; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.RequestProperties; @@ -125,4 +127,4 @@ public Set getCapabilities() { public void setCapabilities(Set capabilities) { this.capabilities = capabilities; } -} +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java index 549307d8..2a1181fb 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateRequest.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java @@ -31,7 +31,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; -public class CertificateRequest implements Serializable { +public class CertificateChoiceSessionRequest implements Serializable { private String relyingPartyUUID; private String relyingPartyName; diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java deleted file mode 100644 index 97da34a7..00000000 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkCertificateChoiceSessionResponse.java +++ /dev/null @@ -1,60 +0,0 @@ -package ee.sk.smartid.v3.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -public class DynamicLinkCertificateChoiceSessionResponse implements Serializable { - - private String sessionID; - private String sessionToken; - private String sessionSecret; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } - - public String getSessionToken() { - return sessionToken; - } - - public void setSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - } - - public String getSessionSecret() { - return sessionSecret; - } - - public void setSessionSecret(String sessionSecret) { - this.sessionSecret = sessionSecret; - } -} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java index 83685067..bf919132 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L @@ -31,6 +31,8 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.v3.SignatureProtocol; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.RequestProperties; @@ -125,4 +127,4 @@ public RequestProperties getRequestProperties() { public void setRequestProperties(RequestProperties requestProperties) { this.requestProperties = requestProperties; } -} +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java index 6f06b28a..7130670f 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -58,10 +58,11 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; -public class DynamicLinkAuthenticationSessionRequestBuilderTest { +class DynamicLinkAuthenticationSessionRequestBuilderTest { private SmartIdConnector connector; @@ -74,7 +75,7 @@ void setUp() { class ValidateRequiredRequestParameters { @Test - public void initAuthenticationSession_ok() { + void initAuthenticationSession_ok() { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -100,7 +101,7 @@ public void initAuthenticationSession_ok() { @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) - public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); @@ -121,7 +122,7 @@ public void initAuthenticationSession_certificateLevel_ok(AuthenticationCertific @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - public void initAuthenticationSession_nonce_ok(String nonce) { + void initAuthenticationSession_nonce_ok(String nonce) { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); @@ -142,7 +143,7 @@ public void initAuthenticationSession_nonce_ok(String nonce) { @ParameterizedTest @EnumSource - public void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); @@ -162,7 +163,7 @@ public void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm s } @Test - public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); @@ -182,7 +183,7 @@ public void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestPrope @ParameterizedTest @ValueSource(booleans = {true, false}) - public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) .thenReturn(createDynamicLinkAuthenticationResponse()); @@ -204,7 +205,7 @@ public void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) - public void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); new DynamicLinkAuthenticationSessionRequestBuilder(connector) @@ -224,7 +225,7 @@ public void initAuthenticationSession_capabilities_ok(String[] capabilities, Set @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID(relyingPartyUUID) @@ -236,7 +237,7 @@ public void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(Str @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -248,7 +249,7 @@ public void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(Str @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { + void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -261,7 +262,7 @@ public void initAuthenticationSession_randomChallengeIsEmpty_throwException(Stri @ParameterizedTest @ArgumentsSource(InvalidRandomChallengeArgumentProvider.class) - public void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { + void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -273,7 +274,7 @@ public void initAuthenticationSession_randomChallengeIsInvalid_throwException(St } @Test - public void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -287,7 +288,7 @@ public void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwExcepti @ParameterizedTest @ArgumentsSource(InvalidNonceProvider.class) - public void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -301,7 +302,7 @@ public void initAuthenticationSession_nonceOutOfBounds_throwException(String inv @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { + void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -314,7 +315,7 @@ public void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwExcep @ParameterizedTest @ArgumentsSource(NotSupportedInteractionsProvider.class) - public void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { + void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -327,7 +328,7 @@ public void initAuthenticationSession_allowedInteractionsOrderContainsNotSupport @ParameterizedTest @ArgumentsSource(InvalidInteractionsProvider.class) - public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { + void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -344,9 +345,9 @@ class ValidateRequiredResponseParameters { @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); @@ -357,9 +358,9 @@ public void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwEx @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { + void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionResponse.setSessionToken(sessionToken); when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); @@ -371,9 +372,9 @@ public void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_thro @ParameterizedTest @NullAndEmptySource - public void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { + void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); @@ -434,8 +435,8 @@ void initAuthenticationSession_withDocumentNumber() { assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); } - private DynamicLinkAuthenticationSessionResponse createDynamicLinkAuthenticationResponse() { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkAuthenticationSessionResponse(); + private DynamicLinkSessionResponse createDynamicLinkAuthenticationResponse() { + var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); dynamicLinkAuthenticationSessionResponse.setSessionSecret(generateBase64String("sessionSecret")); diff --git a/src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java similarity index 59% rename from src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java index 7285ff9d..9890c256 100644 --- a/src/test/java/ee/sk/smartid/v3/service/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.service; +package ee.sk.smartid.v3; /*- * #%L @@ -34,32 +34,23 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Set; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.v3.CertificateLevel; -import ee.sk.smartid.v3.rest.SessionStatusPoller; import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; class DynamicLinkCertificateChoiceSessionRequestBuilderTest { private SmartIdConnector connector; - private SessionStatusPoller sessionStatusPoller; private DynamicLinkCertificateChoiceSessionRequestBuilder builderService; @BeforeEach void setUp() { connector = mock(SmartIdConnector.class); - sessionStatusPoller = mock(SessionStatusPoller.class); builderService = new DynamicLinkCertificateChoiceSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") @@ -70,68 +61,72 @@ void setUp() { @Test void initiateCertificateChoice() { - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateRequest.class)); + verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullRequestProperties() { builderService.withShareMdClientIpAddress(false); - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); - when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).getCertificate(any(CertificateRequest.class)); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + + verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_missingCertificateLevel() { builderService.withCertificateLevel(null); - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); - when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).getCertificate(any(CertificateRequest.class)); + verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_withValidCapabilities() { - Set capabilities = Set.of("SIGN", "AUTH"); - builderService.withCapabilities(capabilities); - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); + builderService.withCapabilities("ADVANCED", "QUALIFIED"); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateRequest.class)); + verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullCapabilities() { - builderService.withCapabilities(null); - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(mockCertificateChoiceResponse()); - when(sessionStatusPoller.fetchFinalSessionStatus(any(String.class))).thenReturn(createSessionStatus()); + builderService.withCapabilities(); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkCertificateChoiceSessionResponse result = builderService.initiateCertificateChoice(); + DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).getCertificate(any(CertificateRequest.class)); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + + verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); } @Nested @@ -139,35 +134,36 @@ class ErrorCases { @Test void initiateCertificateChoice_whenResponseIsNull() { - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(null); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(null); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); } @Test void initiateCertificateChoice_whenSessionIDIsNull() { - var responseWithNullSessionID = new DynamicLinkCertificateChoiceSessionResponse(); + var responseWithNullSessionID = new DynamicLinkSessionResponse(); responseWithNullSessionID.setSessionToken("test-session-token"); responseWithNullSessionID.setSessionSecret("test-session-secret"); - when(connector.getCertificate(any(CertificateRequest.class))).thenReturn(responseWithNullSessionID); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); } @Test void initiateCertificateChoice_userAccountNotFound() { - when(connector.getCertificate(any(CertificateRequest.class))).thenThrow(new UserAccountNotFoundException()); + when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); - assertThrows(UserAccountNotFoundException.class, () -> builderService.initiateCertificateChoice()); + var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); + assertEquals(UserAccountNotFoundException.class, ex.getClass()); } @Test void initiateCertificateChoice_missingRelyingPartyUUID() { builderService.withRelyingPartyUUID(null); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); } @@ -175,7 +171,7 @@ void initiateCertificateChoice_missingRelyingPartyUUID() { void initiateCertificateChoice_missingRelyingPartyName() { builderService.withRelyingPartyName(null); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); } @@ -183,34 +179,24 @@ void initiateCertificateChoice_missingRelyingPartyName() { void initiateCertificateChoice_invalidNonce() { builderService.withNonce("1234567890123456789012345678901"); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters. You supplied: '1234567890123456789012345678901'", ex.getMessage()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); } @Test void initiateCertificateChoice_emptyNonce() { builderService.withNonce(""); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initiateCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters. You supplied: ''", ex.getMessage()); + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); } } - private static DynamicLinkCertificateChoiceSessionResponse mockCertificateChoiceResponse() { - var response = new DynamicLinkCertificateChoiceSessionResponse(); + private static DynamicLinkSessionResponse mockCertificateChoiceResponse() { + var response = new DynamicLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); return response; } - - private SessionStatus createSessionStatus() { - var sessionStatus = new SessionStatus(); - var sessionResult = new SessionResult(); - - sessionResult.setEndResult("OK"); - sessionStatus.setResult(sessionResult); - - return sessionStatus; - } } diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java index f4bd9207..b1d32187 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -59,6 +59,7 @@ import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; class DynamicLinkSignatureSessionRequestBuilderTest { @@ -84,7 +85,7 @@ void initSignatureSession_withSemanticsIdentifier() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); assertEquals("test-session-id", signature.getSessionID()); @@ -104,7 +105,7 @@ void initSignatureSession_withDocumentNumber() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); assertEquals("test-session-id", signature.getSessionID()); @@ -124,7 +125,7 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -143,7 +144,7 @@ void initSignatureSession_withNonce_ok(String nonce) { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -161,7 +162,7 @@ void initSignatureSession_withRequestProperties() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -182,7 +183,7 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -205,7 +206,7 @@ void initSignatureSession_withSignableHash(HashType hashType) { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); @@ -219,12 +220,12 @@ void initSignatureSession_withSignableHash(HashType hashType) { @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities(Set capabilities, Set expectedCapabilities) { + void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -245,7 +246,7 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); @@ -266,7 +267,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(mockSignatureSessionResponse()); - DynamicLinkSignatureSessionResponse signature = builder.initSignatureSession(); + DynamicLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); @@ -381,7 +382,7 @@ class ResponseValidationTests { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionID(String sessionID) { - var response = new DynamicLinkSignatureSessionResponse(); + var response = new DynamicLinkSessionResponse(); response.setSessionID(sessionID); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); @@ -396,7 +397,7 @@ void validateResponseParameters_missingSessionID(String sessionID) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionToken(String sessionToken) { - var response = new DynamicLinkSignatureSessionResponse(); + var response = new DynamicLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken(sessionToken); response.setSessionSecret("test-session-secret"); @@ -411,7 +412,7 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionSecret(String sessionSecret) { - var response = new DynamicLinkSignatureSessionResponse(); + var response = new DynamicLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret(sessionSecret); @@ -424,8 +425,8 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { } } - private DynamicLinkSignatureSessionResponse mockSignatureSessionResponse() { - var response = new DynamicLinkSignatureSessionResponse(); + private DynamicLinkSessionResponse mockSignatureSessionResponse() { + var response = new DynamicLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); @@ -447,9 +448,9 @@ private static class CapabilitiesArgumentProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(Set.of("QUALIFIED", "ADVANCED"), Set.of("QUALIFIED", "ADVANCED")), - Arguments.of(Set.of("QUALIFIED"), Set.of("QUALIFIED")), - Arguments.of(Set.of(), Set.of()) + Arguments.of(new String[]{"QUALIFIED", "ADVANCED"}, Set.of("QUALIFIED", "ADVANCED")), + Arguments.of(new String[]{"QUALIFIED"}, Set.of("QUALIFIED")), + Arguments.of(new String[]{}, Set.of()) ); } } diff --git a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java index 09c137be..16bb8f25 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java @@ -58,6 +58,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.VerificationCode; diff --git a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java index cecca2b1..89959521 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java @@ -60,6 +60,7 @@ import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.VerificationCode; class NotificationSignatureSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java b/src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java new file mode 100644 index 00000000..73c64b7c --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java @@ -0,0 +1,147 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class SignatureUtilTest { + + @Test + void getDigestToSignBase64_withSignableHash() { + var signableHash = new SignableHash(); + signableHash.setHash("Test hash".getBytes()); + signableHash.setHashType(HashType.SHA256); + + String digestBase64 = SignatureUtil.getDigestToSignBase64(signableHash, null); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), digestBase64); + } + + @Test + void getDigestToSignBase64_withSignableData() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(HashType.SHA256); + + String digestBase64 = SignatureUtil.getDigestToSignBase64(null, signableData); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), digestBase64); + } + + @Test + void getDigestToSignBase64_throwsExceptionWhenNoHashOrData() { + var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(null, null)); + assertEquals("Either signableHash or signableData must be set.", exception.getMessage()); + } + + @Test + void getDigestToSignBase64_throwsExceptionWhenHashTypeIsNullInSignableData() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(null); + + var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(null, signableData)); + assertEquals("HashType must be set for signableData.", exception.getMessage()); + } + + @Test + void getDigestToSignBase64_withSignableHashFieldsNotFilled() { + var signableHash = new SignableHash(); + signableHash.setHash(new byte[0]); + signableHash.setHashType(HashType.SHA256); + + var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(signableHash, null)); + assertEquals("Either signableHash or signableData must be set.", exception.getMessage()); + } + + @Test + void getSignatureAlgorithm_withExplicitSignatureAlgorithm() { + String algorithm = SignatureUtil.getSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA, null, null); + assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), algorithm); + } + + @Test + void getSignatureAlgorithm_withSignableHashHashTypeNull() { + var signableHash = new SignableHash(); + signableHash.setHash("Test hash".getBytes()); + signableHash.setHashType(null); + + String algorithm = SignatureUtil.getSignatureAlgorithm(null, signableHash, null); + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void getSignatureAlgorithm_withHashTypeInSignableHash(HashType hashType) { + var signableHash = new SignableHash(); + signableHash.setHashType(hashType); + + String algorithm = SignatureUtil.getSignatureAlgorithm(null, signableHash, null); + assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", algorithm); + } + + @ParameterizedTest + @EnumSource(HashType.class) + void getSignatureAlgorithm_withHashTypeInSignableData(HashType hashType) { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(hashType); + + String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, signableData); + assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", algorithm); + } + + @Test + void getSignatureAlgorithm_withSignableDataHashTypeNull() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(null); + + String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, signableData); + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); + } + + @Test + void getSignatureAlgorithm_withDefaultAlgorithm() { + String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, null); + assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); + } + + @Test + void setHashInBase64_shouldDecodeBase64String() { + var signableHash = new SignableHash(); + String base64EncodedHash = Base64.getEncoder().encodeToString("Test hash".getBytes()); + + signableHash.setHashInBase64(base64EncodedHash); + + assertEquals(base64EncodedHash, signableHash.getHashInBase64()); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index c9e3762c..0032ee21 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -46,7 +46,6 @@ import ee.sk.smartid.FileUtil; import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdRestServiceStubs; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; @@ -75,12 +74,12 @@ void createDynamicLinkCertificateChoice() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initiateCertificateChoice(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -92,50 +91,50 @@ void createDynamicLinkCertificateChoice() { @WireMockTest(httpPort = 18089) class DynamicLinkAuthenticationSession { - @Test - void createDynamicLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } - @Test - void createDynamicLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } - @Test - void createDynamicLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -155,14 +154,14 @@ void createDynamicLinkSignature_withDocumentNumber() { signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -177,14 +176,14 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSignatureSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -302,7 +301,7 @@ class DynamicContent { @EnumSource void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -331,12 +330,12 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) - .initiateCertificateChoice(); + .initCertificateChoice(); Instant sessionResponseReceivedTime = Instant.now(); String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); @@ -357,12 +356,12 @@ void createDynamicContent_createQrCode() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - DynamicLinkCertificateChoiceSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) - .initiateCertificateChoice(); + .initCertificateChoice(); Instant sessionResponseReceivedTime = Instant.now(); String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); diff --git a/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java rename to src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java index 89b227f3..b89a59af 100644 --- a/src/test/java/ee/sk/smartid/v3/service/SmartIdRequestBuilderServiceTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.service; +package ee.sk.smartid.v3; /*- * #%L @@ -66,9 +66,6 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v3.SignableData; -import ee.sk.smartid.v3.SmartIdAuthenticationResponse; -import ee.sk.smartid.v3.SmartIdClient; import ee.sk.smartid.v3.rest.SessionStatusPoller; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.SessionCertificate; @@ -94,7 +91,6 @@ static void loadCertificate() throws IOException { .replace("-----BEGIN CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "") .replaceAll("\\s+", ""); - } } diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 2e9a73bc..27cd939f 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -59,15 +59,13 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.AuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; -import ee.sk.smartid.v3.DynamicLinkSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.v3.SignatureSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SessionStatus; @@ -219,7 +217,7 @@ void setUp() { @Test void initDynamicLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); assertNotNull(response); } @@ -255,7 +253,7 @@ void setUp() { @Test void initDynamicLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); assertNotNull(response); } @@ -291,7 +289,7 @@ void setUp() { @Test void initAnonymousDynamicLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkAuthenticationSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + DynamicLinkSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); assertNotNull(response); } @@ -400,8 +398,8 @@ public void setUp() { void getCertificate() { stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/responses/dynamic-link-certificate-choice-response.json"); - CertificateRequest request = createCertificateRequest(); - DynamicLinkCertificateChoiceSessionResponse response = connector.getCertificate(request); + CertificateChoiceSessionRequest request = createCertificateRequest(); + DynamicLinkSessionResponse response = connector.getCertificate(request); assertNotNull(response); assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); @@ -411,7 +409,7 @@ void getCertificate() { @Test void getCertificate_invalidCertificateLevel_throwsBadRequestException() { - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); request.setCertificateLevel("INVALID_LEVEL"); stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); @@ -423,7 +421,7 @@ void getCertificate_invalidCertificateLevel_throwsBadRequestException() { void getCertificate_userAccountNotFound() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 404); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); assertThrows(UserAccountNotFoundException.class, () -> connector.getCertificate(request)); } @@ -431,7 +429,7 @@ void getCertificate_userAccountNotFound() { void getCertificate_relyingPartyNoPermission() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 403); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); } @@ -439,7 +437,7 @@ void getCertificate_relyingPartyNoPermission() { void getCertificate_invalidRequest() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); - CertificateRequest request = new CertificateRequest(); + CertificateChoiceSessionRequest request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID(""); request.setRelyingPartyName(""); @@ -450,7 +448,7 @@ void getCertificate_invalidRequest() { void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 401); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); @@ -461,7 +459,7 @@ void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthor void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 471); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.getCertificate(request)); } @@ -470,7 +468,7 @@ void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { void getCertificate_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 472); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.getCertificate(request)); } @@ -479,7 +477,7 @@ void getCertificate_throwsPersonShouldViewSmartIdPortalException() { void getCertificate_throwsSmartIdClientException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 480); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); Exception exception = assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); @@ -490,7 +488,7 @@ void getCertificate_throwsSmartIdClientException() { void getCertificate_throwsServerMaintenanceException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 580); - CertificateRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = createCertificateRequest(); assertThrows(ServerMaintenanceException.class, () -> connector.getCertificate(request)); } @@ -515,7 +513,7 @@ void initDynamicLinkSignature_withSemanticsIdentifier_successful() { SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); + DynamicLinkSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); @@ -530,7 +528,7 @@ void initDynamicLinkSignature_withDocumentNumber_successful() { SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111"; - DynamicLinkSignatureSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); + DynamicLinkSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); @@ -862,8 +860,8 @@ private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest( return dynamicLinkAuthenticationSessionRequest; } - private CertificateRequest createCertificateRequest() { - var request = new CertificateRequest(); + private CertificateChoiceSessionRequest createCertificateRequest() { + var request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); request.setCertificateLevel("ADVANCED"); diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java index 724eb626..ade82c88 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java @@ -34,8 +34,8 @@ import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.AuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.RandomChallenge; import ee.sk.smartid.v3.SignatureAlgorithm; import ee.sk.smartid.v3.rest.dao.Interaction; @@ -43,7 +43,7 @@ @Disabled("Currently request to v3 path returns - No permission to issue the request") @SmartIdDemoIntegrationTest -public class SmartIdRestIntegrationTest { +class SmartIdRestIntegrationTest { private SmartIdConnector smartIdConnector; @@ -58,7 +58,7 @@ void authenticate_anonymous() { request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); - DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); + DynamicLinkSessionResponse response = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); } @Test @@ -67,7 +67,7 @@ void authenticate_withDocumentNumber() { request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); - DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-50609019996-MOCK-Q"); + DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-50609019996-MOCK-Q"); } @Test @@ -76,11 +76,11 @@ void authenticate_withSemanticsIdentifier() { request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); - DynamicLinkAuthenticationSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); + DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); } private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { - AuthenticationSessionRequest request = new AuthenticationSessionRequest(); + var request = new AuthenticationSessionRequest(); request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); request.setRelyingPartyName("DEMO"); request.setCertificateLevel("QUALIFIED"); diff --git a/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json b/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json index 098a1060..1bd3d1d8 100644 --- a/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json +++ b/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json @@ -2,8 +2,5 @@ "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "ADVANCED", - "nonce": "cmFuZG9tTm9uY2U=", - "requestProperties": { - "shareMdClientIpAddress": false - } + "nonce": "cmFuZG9tTm9uY2U=" } \ No newline at end of file diff --git a/src/test/resources/v3/requests/dynamic-link-signature-request.json b/src/test/resources/v3/requests/dynamic-link-signature-request.json index c62d3ede..0b82a790 100644 --- a/src/test/resources/v3/requests/dynamic-link-signature-request.json +++ b/src/test/resources/v3/requests/dynamic-link-signature-request.json @@ -11,8 +11,5 @@ "type": "displayTextAndPIN", "displayText60": "Sign document?" } - ], - "requestProperties": { - "shareMdClientIpAddress": false - } + ] } \ No newline at end of file From 2fd07b1781cc9cb83e8576cecaca9e9780aedecb Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Sat, 7 Dec 2024 07:51:51 +0200 Subject: [PATCH 12/57] Added notification based certificate choice request handling (#97) * SLIB-67 - refactoring dynamic link based auth flow * SLIB-67 - removed unnecessary abstract classes, refactored * SLIB-67 - added tests, relocated classes * SLIB-50 - added notification based certificate choice request handling * SLIB-50 - fixed naming and javadoc description --------- Co-authored-by: ragnar.haide --- README.md | 65 +++- ...nkAuthenticationSessionRequestBuilder.java | 6 +- ...ertificateChoiceSessionRequestBuilder.java | 4 +- ...micLinkSignatureSessionRequestBuilder.java | 4 +- ...onAuthenticationSessionRequestBuilder.java | 6 +- ...ertificateChoiceSessionRequestBuilder.java | 239 +++++++++++++ ...ationCertificateChoiceSessionResponse.java | 42 +++ ...icationSignatureSessionRequestBuilder.java | 4 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 11 + .../sk/smartid/v3/rest/SmartIdConnector.java | 33 +- .../smartid/v3/rest/SmartIdRestConnector.java | 40 ++- ...thenticationSessionRequestBuilderTest.java | 2 +- ...thenticationSessionRequestBuilderTest.java | 2 +- ...ficateChoiceSessionRequestBuilderTest.java | 332 ++++++++++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 41 ++- .../v3/rest/SmartIdRestConnectorTest.java | 112 +++++- ...> certificate-choice-session-request.json} | 0 ...n-certificate-choice-session-response.json | 3 + 18 files changed, 905 insertions(+), 41 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java create mode 100644 src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java rename src/test/resources/v3/requests/{dynamic-link-certificate-choice-request.json => certificate-choice-session-request.json} (100%) create mode 100644 src/test/resources/v3/responses/notification-certificate-choice-session-response.json diff --git a/README.md b/README.md index 90e7db88..7a012814 100644 --- a/README.md +++ b/README.md @@ -966,6 +966,67 @@ client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); .initiateCertificateChoice(); ``` +## Initiating a Notification Certificate Choice in API v3.0 + +### Request Parameters +The request parameters for the dynamic link certificate choice session are: + +* `relyingPartyUUID`: UUID of the Relying Party. +* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. + +### Using Semantics Identifier +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "30303039914"); // identifier (according to country and identity type reference) + +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withNonce("1234567890") + .initCertificateChoice(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later +``` + +### Using Document Number +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withNonce("1234567890") + .initCertificateChoice(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later +``` + +### Requesting the IP Address of the User's Device +If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withShareMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. +```java +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QUALIFIED) + + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initCertificateChoice(); +``` + # Initiating a Dynamic Link Signature Session in API v3.0 The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a signature session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the signing process using a dynamic link. The user can then access the link and complete the signing process. @@ -1518,7 +1579,7 @@ String verificationCode = authenticationSessionResponse.getVc().getValue(); ``` ### Requesting the IP Address of the User's Device -If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withSharedMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. +If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withShareMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. ```java NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() @@ -1528,7 +1589,7 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client .withAllowedInteractionsOrder(Collections.singletonList( Interaction.verificationCodeChoice("Log in to self-service?") )) - .withSharedMdClientIpAddress(true) // Request the user's device IP address + .withShareMdClientIpAddress(true) // Request the user's device IP address .initAuthenticationSession(); ``` diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java index e7af37d3..3896f594 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -156,12 +156,12 @@ public DynamicLinkAuthenticationSessionRequestBuilder withAllowedInteractionsOrd } /** - * Sets whether to share the Mobile-ID client IP address + * Sets whether to share the Mobile device IP address * - * @param shareMdClientIpAddress whether to share the Mobile-ID client IP address + * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder */ - public DynamicLinkAuthenticationSessionRequestBuilder withSharedMdClientIpAddress(boolean shareMdClientIpAddress) { + public DynamicLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; return this; } diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 95213065..22c59584 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -115,10 +115,10 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withCapabilities(String } /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. + * Sets whether to share the Mobile device IP address * + * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder - * @see Mobile Device IP sharing */ public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java index 6c17f9e8..58ed2293 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java @@ -164,10 +164,10 @@ public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(Li } /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. + * Sets whether to share the Mobile device IP address * + * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder - * @see Mobile Device IP sharing */ public DynamicLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java index f27a5712..365d22b9 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java @@ -158,12 +158,12 @@ public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOr } /** - * Sets whether to share the Mobile-ID client IP address + * Sets whether to share the Mobile device IP address * - * @param shareMdClientIpAddress whether to share the Mobile-ID client IP address + * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder */ - public NotificationAuthenticationSessionRequestBuilder withSharedMdClientIpAddress(boolean shareMdClientIpAddress) { + public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; return this; } diff --git a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java new file mode 100644 index 00000000..6b07eb63 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java @@ -0,0 +1,239 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +import java.util.Set; + +public class NotificationCertificateChoiceSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationCertificateChoiceSessionRequestBuilder.class); + + private final SmartIdConnector connector; + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private Boolean shareMdClientIpAddress; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + + /** + * Constructs a new NotificationCertificateChoiceSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the document number + *

      + * Setting this value will make the notification session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier + *

      + * Setting this value will make the notification session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sends the notification request and get the init session response + *

      + * There are 2 supported ways to start authentication session: + *

        + *
      • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
      • + *
      • with document number by using {@link #withDocumentNumber(String)}
      • + *
      + * + * @return init session response + */ + public NotificationCertificateChoiceSessionResponse initCertificateChoice() { + validateRequestParameters(); + CertificateChoiceSessionRequest request = createCertificateChoiceRequest(); + NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); + validateResponseParameters(notificationCertificateChoiceSessionResponse); + return notificationCertificateChoiceSessionResponse; + } + + private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(CertificateChoiceSessionRequest request) { + if (semanticsIdentifier != null) { + return connector.initNotificationCertificateChoice(request, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initNotificationCertificateChoice(request, documentNumber); + } else { + throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set."); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + validateNonce(); + } + + private CertificateChoiceSessionRequest createCertificateChoiceRequest() { + var request = new CertificateChoiceSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + + request.setNonce(nonce); + + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + request.setRequestProperties(requestProperties); + } + + request.setCapabilities(capabilities); + return request; + } + + private void validateNonce() { + if (nonce == null) { + return; + } + if (nonce.isEmpty()) { + logger.error("Parameter nonce value has to be at least 1 character long"); + throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); + } + if (nonce.length() > 30) { + logger.error("Nonce cannot be longer that 30 chars"); + throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); + } + } + + private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java new file mode 100644 index 00000000..b63eaa45 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java @@ -0,0 +1,42 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class NotificationCertificateChoiceSessionResponse implements Serializable { + + private String sessionID; + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java index 38a8c0cd..0fb17eb4 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java @@ -164,10 +164,10 @@ public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(L } /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. + * Sets whether to share the Mobile device IP address * + * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder - * @see Mobile Device IP sharing */ public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 859d8d6a..dcacb304 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -74,6 +74,17 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertif return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()); } + /** + * Creates a new builder for creating a notification certificate choice session request. + * + * @return a builder for creating a new notification certificate choice session request + */ + public NotificationCertificateChoiceSessionRequestBuilder createNotificationCertificateChoice() { + return new NotificationCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + /** * Creates a new builder for creating a new dynamic link authentication session request * diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index bdad790c..95932178 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,10 +32,11 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -63,26 +64,44 @@ public interface SmartIdConnector extends Serializable { /** * Initiates a dynamic link based certificate choice request. * - * @param request CertificateRequest containing necessary parameters - * @return CertificateChoiceResponse containing sessionID, sessionToken, and sessionSecret + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest request); + /** + * Initiates a notification based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @param semanticsIdentifier The semantics identifier + * @return NotificationCertificateChoiceSessionResponse containing sessionID + */ + NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a notification based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @param documentNumber The document number + * @return NotificationCertificateChoiceSessionResponse containing sessionID + */ + NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, String documentNumber); + /** * Initiates a dynamic link based signature sessions. * - * @param request DynamicLinkSignatureSessionRequest containing necessary parameters for the signature session + * @param request SignatureSessionRequest containing necessary parameters for the signature session * @param semanticsIdentifier The semantics identifier - * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret + * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a dynamic link based signature sessions. * - * @param request DynamicLinkSignatureSessionRequest containing necessary parameters for the signature session + * @param request SignatureSessionRequest containing necessary parameters for the signature session * @param documentNumber The document number - * @return DynamicLinkSignatureSessionResponse containing sessionID, sessionToken, and sessionSecret + * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index fd4f6552..6e007b74 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -49,6 +49,7 @@ import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.v3.DynamicLinkSessionResponse; import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; @@ -77,6 +78,9 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String SESSION_STATUS_URI = "/session/{sessionId}"; private static final String CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH = "/certificatechoice/dynamic-link/anonymous"; + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_DOCUMENT_NUMBER_PATH = "/certificatechoice/notification/document"; + private static final String DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/dynamic-link/etsi"; private static final String DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/dynamic-link/document"; @@ -181,7 +185,27 @@ public DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest .path(CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH) .build(); - return postCertificateChoiceRequest(uri, request); + return postDynamicLinkCertificateChoiceRequest(uri, request); + } + + @Override + public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postNotificationCertificateChoiceRequest(uri, request); + } + + @Override + public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postNotificationCertificateChoiceRequest(uri, request); } @Override @@ -297,7 +321,7 @@ private NotificationAuthenticationSessionResponse postNotificationAuthentication } } - private DynamicLinkSessionResponse postCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { + private DynamicLinkSessionResponse postDynamicLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { try { return postRequest(uri, request, DynamicLinkSessionResponse.class); } catch (NotFoundException ex) { @@ -309,6 +333,18 @@ private DynamicLinkSessionResponse postCertificateChoiceRequest(URI uri, Certifi } } + private NotificationCertificateChoiceSessionResponse postNotificationCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { + try { + return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); + } catch (NotFoundException ex) { + logger.warn("User account not found for URI {}", uri, ex); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } + } + private DynamicLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { return postRequest(uri, request, DynamicLinkSessionResponse.class); diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java index 7130670f..a5464ca7 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -192,7 +192,7 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) - .withSharedMdClientIpAddress(ipRequested) + .withShareMdClientIpAddress(ipRequested) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); diff --git a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java index 16bb8f25..3359131b 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java @@ -172,7 +172,7 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { .withRandomChallenge(generateBase64String("a".repeat(32))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) - .withSharedMdClientIpAddress(ipRequested) + .withShareMdClientIpAddress(ipRequested) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); diff --git a/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java new file mode 100644 index 00000000..b2eaff65 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -0,0 +1,332 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; + +class NotificationCertificateChoiceSessionRequestBuilderTest { + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + void initCertificateChoiceSession_withSemanticsIdentifier() { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .initCertificateChoice(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @Test + void initCertificateChoiceSession_withDocumentNumber() { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(String.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .initCertificateChoice(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(certificateLevel) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + CertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initCertificateChoiceSession_nonce_ok(String nonce) { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(nonce) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + CertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.getNonce()); + } + + @Test + void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + CertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNull(request.getRequestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initCertificateChoiceSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withShareMdClientIpAddress(ipRequested) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + CertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.getRequestProperties()); + assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withCapabilities(capabilities) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + CertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.getCapabilities()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName("DEMO") + .initCertificateChoice()); + assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName(relyingPartyName) + .initCertificateChoice()); + assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidNonceProvider.class) + void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(invalidNonce) + .initCertificateChoice()); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initCertificateChoiceSession_semanticsIdentifierOrDocumentNumberMissing_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .initCertificateChoice()); + assertEquals("Either documentNumber or semanticsIdentifier must be set.", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var exception = assertThrows(SmartIdClientException.class, () -> { + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); + notificationCertificateChoiceSessionResponse.setSessionID(sessionId); + when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); + + new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .initCertificateChoice(); + }); + assertEquals("Session ID is missing from the response", exception.getMessage()); + } + } + + private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); + notificationCertificateChoiceSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + return notificationCertificateChoiceSessionResponse; + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[0], Collections.emptySet()), + Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), + Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) + ); + } + } + + private static class InvalidNonceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") + ); + } + } +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 0032ee21..9200d712 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -71,7 +71,7 @@ class DynamicLinkCertificateChoiceSession { @Test void createDynamicLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() @@ -87,6 +87,41 @@ void createDynamicLinkCertificateChoice() { } } + @Nested + @WireMockTest(httpPort = 18089) + class NotificationCertificateChoiceSession { + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .initCertificateChoice(); + + assertNotNull(response.getSessionID()); + } + + @Test + void createNotificationCertificateChoice_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .initCertificateChoice(); + + assertNotNull(response.getSessionID()); + } + } + @Nested @WireMockTest(httpPort = 18089) class DynamicLinkAuthenticationSession { @@ -327,7 +362,7 @@ void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLin @ParameterizedTest @EnumSource void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() @@ -353,7 +388,7 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/dynamic-link-certificate-choice-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 27cd939f..73fdcf78 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -63,6 +63,7 @@ import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; @@ -385,7 +386,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { @Nested @WireMockTest(httpPort = 18089) - class CertificateChoiceTests { + class DynamicLinkCertificateChoiceTests { private SmartIdConnector connector; @@ -398,7 +399,7 @@ public void setUp() { void getCertificate() { stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/responses/dynamic-link-certificate-choice-response.json"); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); DynamicLinkSessionResponse response = connector.getCertificate(request); assertNotNull(response); @@ -409,7 +410,7 @@ void getCertificate() { @Test void getCertificate_invalidCertificateLevel_throwsBadRequestException() { - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); request.setCertificateLevel("INVALID_LEVEL"); stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); @@ -421,7 +422,7 @@ void getCertificate_invalidCertificateLevel_throwsBadRequestException() { void getCertificate_userAccountNotFound() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 404); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(UserAccountNotFoundException.class, () -> connector.getCertificate(request)); } @@ -429,7 +430,7 @@ void getCertificate_userAccountNotFound() { void getCertificate_relyingPartyNoPermission() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 403); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); } @@ -448,7 +449,7 @@ void getCertificate_invalidRequest() { void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 401); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); @@ -459,7 +460,7 @@ void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthor void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 471); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.getCertificate(request)); } @@ -468,7 +469,7 @@ void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { void getCertificate_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 472); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.getCertificate(request)); } @@ -477,7 +478,7 @@ void getCertificate_throwsPersonShouldViewSmartIdPortalException() { void getCertificate_throwsSmartIdClientException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 480); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Exception exception = assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); @@ -488,12 +489,96 @@ void getCertificate_throwsSmartIdClientException() { void getCertificate_throwsServerMaintenanceException() { stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 580); - CertificateChoiceSessionRequest request = createCertificateRequest(); + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(ServerMaintenanceException.class, () -> connector.getCertificate(request)); } } + @Nested + @WireMockTest(httpPort = 18089) + class SemanticsIdentifierNotificationCertificateChoiceTests { + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initCertificateChoice_withSemanticsIdentifier_successful() { + stubPostRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/responses/notification-certificate-choice-session-response.json"); + + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("test-session-id", response.getSessionID()); + } + + @Test + void initCertificateChoice_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/requests/certificate-choice-session-request.json"); + connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); + }); + } + + @Test + void initCertificateChoice_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/requests/certificate-choice-session-request.json"); + connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); + }); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DocumentNumberNotificationCertificateChoiceTests { + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initCertificateChoice_withDocumentNumber_successful() { + stubPostRequestWithResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/responses/notification-certificate-choice-session-response.json"); + + CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + String documentNumber = "PNOEE-48010010101-MOCK-Q"; + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, documentNumber); + + assertNotNull(response); + assertEquals("test-session-id", response.getSessionID()); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + assertThrows(UserAccountNotFoundException.class, () -> { + SmartIdRestServiceStubs.stubNotFoundResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/certificate-choice-session-request.json"); + connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + + @Test + void initNotificationAuthentication_requestIsUnauthorized_throwException() { + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + SmartIdRestServiceStubs.stubForbiddenResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/certificate-choice-session-request.json"); + connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + }); + } + } + @Nested @WireMockTest(httpPort = 18089) class DynamicLinkSignatureTests { @@ -860,11 +945,12 @@ private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest( return dynamicLinkAuthenticationSessionRequest; } - private CertificateChoiceSessionRequest createCertificateRequest() { + private CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); + request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + request.setRelyingPartyName("DEMO"); request.setCertificateLevel("ADVANCED"); + request.setNonce("cmFuZG9tTm9uY2U="); return request; } diff --git a/src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json b/src/test/resources/v3/requests/certificate-choice-session-request.json similarity index 100% rename from src/test/resources/v3/requests/dynamic-link-certificate-choice-request.json rename to src/test/resources/v3/requests/certificate-choice-session-request.json diff --git a/src/test/resources/v3/responses/notification-certificate-choice-session-response.json b/src/test/resources/v3/responses/notification-certificate-choice-session-response.json new file mode 100644 index 00000000..e41664bd --- /dev/null +++ b/src/test/resources/v3/responses/notification-certificate-choice-session-response.json @@ -0,0 +1,3 @@ +{ + "sessionID": "test-session-id" +} \ No newline at end of file From 81e48f519bf882db8584a344b161db378b959093 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 13 Dec 2024 16:12:21 +0200 Subject: [PATCH 13/57] SLIB-67 - improving integration (#96) * SLIB-67 - fix builders not using RP name and UUID provided in SmartIdClient * SLIB-67 - remove redundant SemanticsIdentifier * SLIB-67 - divide interaction to be dynamic link and notification based * SLIB-67 - provide a way to query session status once * SLIB-67 - refacto dynamic-link authentication response mapping and validation * SLIB-67 - fix hashing auth code * SLIB-67 - refacto AuthenticationIdentity * SLIB-67 - implement AuthenticationIdentity mapper; add tests and documentation * fix merge issues * SLIB-67 - fix document number used in authentication response mapper test * SLIB-67 - move signature response object validation and creation to mapper * SLIB-67 - add comparing of certificate level to authentication response validator * SLIB-67 - move classes to correct packages * SLIB-67 - remove redundant code * SLIB-67 - remove empty files * SLIB-67 - add license header * SLIB-67 - removed deprecated functions from AuthenticationIdentity * SLIB-67 - update v3.0 changelog --- CHANGELOG.md | 21 +- README.md | 217 ++++++---- .../{v2 => }/AuthenticationIdentity.java | 23 +- .../smartid/AuthenticationIdentityMapper.java | 62 +++ .../rest/dao/SemanticsIdentifier.java | 2 +- .../util/CertificateAttributeUtil.java | 4 +- .../util/NationalIdentityNumberUtil.java | 4 +- .../v2/AuthenticationRequestBuilder.java | 2 +- .../v2/AuthenticationResponseValidator.java | 22 +- .../smartid/v2/CertificateRequestBuilder.java | 2 +- .../smartid/v2/SignatureRequestBuilder.java | 2 +- .../sk/smartid/v2/SmartIdRequestBuilder.java | 2 +- .../sk/smartid/v2/rest/SmartIdConnector.java | 2 +- .../smartid/v2/rest/SmartIdRestConnector.java | 2 +- src/main/java/ee/sk/smartid/v3/AuthCode.java | 23 +- .../v3/AuthenticationCertificateLevel.java | 23 +- .../v3/AuthenticationResponseValidator.java | 240 +++++++++++ .../v3/DynamicLinkAuthenticationResponse.java | 147 +++++++ ...namicLinkAuthenticationResponseMapper.java | 163 ++++++++ ...nkAuthenticationSessionRequestBuilder.java | 30 +- ...ertificateChoiceSessionRequestBuilder.java | 1 + ...micLinkSignatureSessionRequestBuilder.java | 21 +- .../ee/sk/smartid/v3/ErrorResultHandler.java | 66 +++ ...onAuthenticationSessionRequestBuilder.java | 23 +- ...ertificateChoiceSessionRequestBuilder.java | 3 +- ...icationSignatureSessionRequestBuilder.java | 21 +- .../smartid/v3/SignatureResponseMapper.java | 179 ++++++++ ...onResponse.java => SingatureResponse.java} | 2 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 35 +- .../v3/SmartIdRequestBuilderService.java | 260 ------------ .../smartid/v3/rest/SessionStatusPoller.java | 31 +- .../sk/smartid/v3/rest/SmartIdConnector.java | 12 +- .../smartid/v3/rest/SmartIdRestConnector.java | 12 +- .../AcspV1SignatureProtocolParameters.java | 2 +- .../dao/AuthenticationSessionRequest.java | 9 +- .../v3/rest/dao/DynamicLinkInteraction.java | 63 +++ ...r.java => DynamicLinkInteractionFlow.java} | 56 +-- .../dao}/DynamicLinkSessionResponse.java | 2 +- .../sk/smartid/v3/rest/dao/Interaction.java | 96 ++--- .../smartid/v3/rest/dao/InteractionFlow.java | 27 +- ...ficationAuthenticationSessionResponse.java | 3 +- ...ationCertificateChoiceSessionResponse.java | 2 +- .../v3/rest/dao/NotificationInteraction.java | 63 +++ .../rest/dao/NotificationInteractionFlow.java | 50 +++ .../NotificationSignatureSessionResponse.java | 3 +- .../RawDigestSignatureProtocolParameters.java | 2 +- .../v3/rest/dao/SignatureSessionRequest.java | 9 +- .../AuthenticationIdentityMapperTest.java | 66 +++ .../ee/sk/smartid/integration/ReadmeTest.java | 4 +- .../integration/SmartIdIntegrationTest.java | 4 +- .../v2/AuthenticationIdentityTest.java | 20 +- .../v2/AuthenticationRequestBuilderTest.java | 2 +- .../AuthenticationResponseValidatorTest.java | 1 + .../v2/CertificateRequestBuilderTest.java | 2 +- .../ee/sk/smartid/v2/SmartIdClientTest.java | 2 +- .../v2/rest/SessionStatusPollerTest.java | 4 +- .../smartid/v2/rest/SmartIdConnectorSpy.java | 3 +- .../v2/rest/SmartIdRestConnectorTest.java | 2 +- .../v2/rest/SmartIdRestIntegrationTest.java | 2 +- .../v2/rest/dao/SemanticsIdentifierTest.java | 2 +- .../v2/util/CertificateAttributeUtilTest.java | 2 +- .../util/NationalIdentityNumberUtilTest.java | 4 +- .../java/ee/sk/smartid/v3/AuthCodeTest.java | 30 +- .../AuthenticationResponseValidatorTest.java | 285 +++++++++++++ .../smartid/v3/DynamicContentBuilderTest.java | 12 +- ...cLinkAuthenticationResponseMapperTest.java | 344 +++++++++++++++ ...thenticationSessionRequestBuilderTest.java | 79 ++-- ...ficateChoiceSessionRequestBuilderTest.java | 1 + ...inkSignatureSessionRequestBuilderTest.java | 30 +- .../sk/smartid/v3/ErrorResultHandlerTest.java | 91 ++++ ...thenticationSessionRequestBuilderTest.java | 83 ++-- ...ficateChoiceSessionRequestBuilderTest.java | 3 +- ...ionSignatureSessionRequestBuilderTest.java | 29 +- .../v3/SignatureResponseMapperTest.java | 251 +++++++++++ .../ee/sk/smartid/v3/SmartIdClientTest.java | 149 ++++--- .../v3/SmartIdRequestBuilderServiceTest.java | 394 ------------------ .../v3/rest/SessionStatusPollerTest.java | 81 ++++ .../v3/rest/SmartIdRestConnectorTest.java | 28 +- .../v3/rest/SmartIdRestIntegrationTest.java | 20 +- .../test-certs/auth-cert-40504040001.pem.crt | 38 ++ src/test/resources/test-certs/ca-cert.pem.crt | 23 + .../resources/test-certs/expired-cert.pem.crt | 39 ++ .../test-certs/other-auth-cert.pem.crt | 39 ++ ...c-link-authentication-session-request.json | 6 +- ...-link-authentication-session-response.json | 2 +- .../v3/responses/session-status-running.json | 4 + 86 files changed, 2889 insertions(+), 1338 deletions(-) rename src/main/java/ee/sk/smartid/{v2 => }/AuthenticationIdentity.java (88%) create mode 100644 src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java rename src/main/java/ee/sk/smartid/{v2 => }/rest/dao/SemanticsIdentifier.java (98%) rename src/main/java/ee/sk/smartid/{v2 => }/util/CertificateAttributeUtil.java (98%) rename src/main/java/ee/sk/smartid/{v2 => }/util/NationalIdentityNumberUtil.java (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java create mode 100644 src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java create mode 100644 src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java create mode 100644 src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java rename src/main/java/ee/sk/smartid/v3/{SmartIdAuthenticationResponse.java => SingatureResponse.java} (98%) delete mode 100644 src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/AcspV1SignatureProtocolParameters.java (98%) create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java rename src/main/java/ee/sk/smartid/v3/rest/dao/{SemanticsIdentifier.java => DynamicLinkInteractionFlow.java} (50%) rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/DynamicLinkSessionResponse.java (98%) rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/NotificationAuthenticationSessionResponse.java (95%) rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/NotificationCertificateChoiceSessionResponse.java (97%) create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java create mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/NotificationSignatureSessionResponse.java (95%) rename src/main/java/ee/sk/smartid/v3/{ => rest/dao}/RawDigestSignatureProtocolParameters.java (98%) create mode 100644 src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java delete mode 100644 src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java create mode 100644 src/test/resources/test-certs/auth-cert-40504040001.pem.crt create mode 100644 src/test/resources/test-certs/ca-cert.pem.crt create mode 100644 src/test/resources/test-certs/expired-cert.pem.crt create mode 100644 src/test/resources/test-certs/other-auth-cert.pem.crt create mode 100644 src/test/resources/v3/responses/session-status-running.json diff --git a/CHANGELOG.md b/CHANGELOG.md index eccec6a1..6ef29b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [3.0] - 2023-10-14 ### Added -- Support for Smart-ID API v3.0 has been added under the ee.sk.smartid.v3 package. -- Added handling for dynamic-link authentication session requests. View V3 section in README.md for more information. -- Added support for handling session status requests. View V3 section in README.md for more information. -- Added support for handling dynamic link certificate choice requests. View V3 section in README.md for more information. +- Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 + package. + - New builder classes to start dynamic-link sessions: + - DynamicLinkAuthenticationSessionRequestBuilder + - DynamicLinkCertificateChoiceSessionRequestBuilder + - DynamicLinkSignatureSessionRequestBuilder + - Helper class for generating authCode used in generating dynamic link - AuthCode#generateAuthCode() + - Helper class for generating Qr-code - QrCodeGenerator + - Helper class for building and generating dynamic-link and/or QR-code - DynamicContentBuilder + - Sessions status request handling for the v3 path. + - Helper class for validating completed auth session status response - DynamicLinkAuthenticationResponseMapper + - Constructing AuthenticationIdentity from DynamicLinkAuthenticationResponse - AuthenticationResponseValidator ### Changed -- Existing code for Smart-ID API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. +- Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. - Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` - Typo fixes, code cleanup and improvements - Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. +### Removed +- Removed deprecated methods from AuthenticationIdentity + ### Java and dependency updates - Updated java to version 17 - Updated slf4j-api to version 2.0.16 diff --git a/README.md b/README.md index 7a012814..1f37cbc3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,17 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Configuring a proxy using JBoss Resteasy library](#configuring-a-proxy-using-jboss-resteasy-library) * [Configuring a proxy using Jersey](#configuring-a-proxy-using-jersey) * [How to use it with RP API v3.0](#how-to-use-api-v30) + * [Initating authentication session](#examples-of-performing-dynamic-link-authentication) + * [Initiating anonymous authentication session](#initiating-anonymous-authentication-session) + * [Initiating authentication session with semantics identifier](#initiating-authentication-session-with-semantics-identifier) + * [Initiating authentication session with document number](#initiating-authentication-session-with-document-number) + * [Querying sessions status](#session-status-request-handling-for-v30) + * [Sessions status response](#session-status-response) + * [Example of fetching session status in v3.0](#example-of-fetching-session-status-in-v30) + * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) + * [Example of querying sessions status](#example-of-querying-sessions-status) + * [Validating sessions status response](#validating-session-status-response) + * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) * [Generating dynamic link ](#generating-dynamic-link) * [Dynamic link parameters](#dynamic-link-parameters) @@ -333,7 +344,7 @@ This may trigger a notification to all the user's devices if user has more than All Smart-ID devices support displaying text that is up to 60 characters long. Some devices also support displaying text (on a separate screen) that is up to 200 characters long -as well as other interaction flows like user needs to choose the correct code from 3 different verification codes. +as well as other interactionDeprecated flows like user needs to choose the correct code from 3 different verification codes. You can send different interactions to user's device and it picks the first one that the app can handle. @@ -366,24 +377,24 @@ SmartIdSignature smartIdSignature = client byte[] signature = smartIdSignature.getValue(); -smartIdSignature.getInteractionFlowUsed(); // which interaction was used +smartIdSignature.getInteractionFlowUsed(); // which interactionDeprecated was used ``` # Setting the order of preferred interactions for displaying text and asking PIN -The app can support different interaction flows and a Relying Party can demand a particular flow with or without a fallback possibility. -Different interaction flows can support different amount of data to display information to user. +The app can support different interactionDeprecated flows and a Relying Party can demand a particular flow with or without a fallback possibility. +Different interactionDeprecated flows can support different amount of data to display information to user. Available interactions: -* `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. +* `displayTextAndPIN` with `displayText60`. The simplest interactionDeprecated with max 60 chars of text and PIN entry on a single screen. Every app has this interactionDeprecated available. * `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. * `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. * `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. -The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interaction supported by the app is taken from `allowedInteractionsOrder` list and sent to client. -The interaction that was actually used is reported back to RP with interactionUsed response parameter to the session request. -If the app cannot support any interaction requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. +The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interactionDeprecated supported by the app is taken from `allowedInteractionsOrder` list and sent to client. +The interactionDeprecated that was actually used is reported back to RP with interactionUsed response parameter to the session request. +If the app cannot support any interactionDeprecated requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. `displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. @@ -394,7 +405,7 @@ Following allowedInteractionsOrder combinations are most likely to be used. ### Short confirmation message with PIN If confirmation message fits to 60 characters then this is the most common choice. -Every Smart-ID app supports this interaction flow and there is no need to provide any fallbacks to this interaction. +Every Smart-ID app supports this interactionDeprecated flow and there is no need to provide any fallbacks to this interactionDeprecated. ```java SmartIdSignature smartIdSignature = client @@ -438,7 +449,7 @@ catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException ### Long confirmation message with fallback to PIN Relying Party first choice is confirmationMessage that can be up to 200 characters long. -If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interaction. +If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interactionDeprecated. ```java @@ -512,7 +523,7 @@ try { .sign(); } catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("User's Smart-ID app is not capable of displaying required interaction"); + System.out.println("User's Smart-ID app is not capable of displaying required interactionDeprecated"); } ``` @@ -674,46 +685,9 @@ var client = new SmartIdClient(); ## Dynamic Link flows -### Examples of performing authentication +### Examples of performing dynamic link authentication -#### Initiating authentication session with document number - -If you already know the documentNumber you can use this for (re-)authentication. - -```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication - -// For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); - -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later - -String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -// Store sessionSecret only on backend side. Do not expose it to the client side. - -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse -``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. - -### Initiating anonymous authentication session +#### Initiating anonymous authentication session Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. RP can learn the user's identity only after the user has authenticated themselves. @@ -788,6 +762,44 @@ String sessionSecret = authenticationSessionResponse.getSessionSecret(); ``` Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +#### Initiating authentication session with document number + +Authentication with document number is mostly for re-authentication. +After the user has authenticated once, the document number is returned in the authentication response. `todo: check if this is correct` + +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication + +// For security reasons a new hash value must be created for each new authentication request +String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + // Smart-ID app will display verification code to the user and user must insert PIN1 + .withAllowedInteractionsOrder( + Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +// Store sessionSecret only on backend side. Do not expose it to the client side. + +// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. + ## Session status request handling for v3.0 The Smart-ID v3.0 API includes new session status request paths for retrieving session results. @@ -804,11 +816,12 @@ The session status response includes various fields depending on whether the ses * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm * `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). * `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionFlowUsed`: The interaction flow used for the session. +* `interactionFlowUsed`: The interactionDeprecated flow used for the session. * `deviceIpAddress`: IP address of the mobile device, if requested. ## Example of fetching session status in v3.0 +### Example of using session status poller to query final sessions status The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. ```java @@ -818,12 +831,13 @@ client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); // Client setup with TrustStore. Requests will not work without a valid certificate. - InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - client.setTrustStore(trustStore); +InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); +client.setTrustStore(trustStore); -var poller = new SessionStatusPoller(client.getSmartIdConnector(), new SmartIdRequestBuilderService()); +// +SessionsStatusPoller poller = client.getSessionsStatusPoller(); SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { @@ -831,6 +845,34 @@ if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { } ``` +### Example of querying sessions status +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Client setup with TrustStore. Requests will not work without a valid certificate. +InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); +client.setTrustStore(trustStore); + +// Get the session status poller +SessionsStatusPoller poller = client.getSessionsStatusPoller(); + +// Queryinn +SessionStatus sessionStatus = poller.getSessionsStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); +if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + // Session is still running and querying can be continued + // Dynamic content can be generated and displayed to the user +} else { + // continue to the next step +} +``` + ## Validating session status response It's important to validate the session status response to ensure that the returned signature or authentication result is valid. @@ -840,6 +882,37 @@ It's important to validate the session status response to ensure that the return * For `ACSP_V1` signature validation, compare the digest of the signature protocol, server random, and random challenge. * For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. +### Example of validating the authentication sessions response: + +```java +// init authentication response validator with trusted certificates +// there are 4 different ways to initialize the validator +// 1. use default values `trusted_certificates.jks` with password `changeit` +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); +// 2. provide your own path to truststore and truststore password +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(truststorePath, truststorePassword); +// 3 read trusted certificate yourself and provide it to the validator +X509Certificate[] trustedCertificates = findTrustedCertificates(); +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCertificates); +// 4. init authentication response validator with the empty array and add trusted certificates +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[0]); +X509Certificate certificate = getTrustedCertificate(); +authenticationResponseValidator.addTrustedCACertificate(certificate); + +// get sessions result +SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); + +// validate sessions state is completed +if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + // validate sessions status result and map session status to authentication response + DynamicLinkAuthenticationResponse response = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.from(response, "randomChallenge"); +} +``` + ### Example of validating the signature: ```java @@ -853,14 +926,14 @@ System.out.println("Authentication result: " + response.getEndResult()); ## Error handling for session status -The session status response may return various error codes indicating the outcome of the session. Below are the possible endResult values for a completed session: +The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: * `OK`: Session completed successfully. * `USER_REFUSED`: User refused the session. * `TIMEOUT`: User did not respond in time. * `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. * `WRONG_VC`: User selected the wrong verification code. -* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interactionDeprecated is not supported by the user's app. * `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. * `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). * `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. @@ -1042,9 +1115,9 @@ The request parameters for the dynamic link signature session are as follows: * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. -* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. +* `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. + * Each interactionDeprecated object includes: + * `type`: Required. Type of interactionDeprecated. Allowed types are `displayTextAndPIN`, `confirmationMessage`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. * `requestProperties`: requestProperties: @@ -1052,7 +1125,7 @@ The request parameters for the dynamic link signature session are as follows: * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. ## Examples of Allowed Interactions Order -An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. Different interactions can support different amounts of data to display information to the user. +An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. Different interactions can support different amounts of data to display information to the user. Below are examples of `allowedInteractionsOrder` elements in JSON format: Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` @@ -1074,7 +1147,7 @@ builder.withAllowedInteractionsOrder(List.of( ``` Example 3: `displayTextAndPIN` Only -Description: Use `displayTextAndPIN` interaction only. +Description: Use `displayTextAndPIN` interactionDeprecated only. ```java builder.withAllowedInteractionsOrder(List.of( Interaction.displayTextAndPIN("Up to 60 characters of text here..") @@ -1195,7 +1268,7 @@ System.out.println("User account not found."); } catch (RelyingPartyAccountConfigurationException e) { System.out.println("Relying party account configuration issue."); } catch (RequiredInteractionNotSupportedByAppException e) { -System.out.println("The required interaction is not supported by the user's app."); +System.out.println("The required interactionDeprecated is not supported by the user's app."); } catch (ServerMaintenanceException e) { System.out.println("Server maintenance in progress, please try again later."); } catch (SmartIdClientException e) { @@ -1204,7 +1277,7 @@ System.out.println("An error occurred: " + e.getMessage()); ``` ## Additional Information -* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interaction it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. +* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. ```java builder.withAllowedInteractionsOrder(List.of( @@ -1257,7 +1330,7 @@ The Smart-ID API v3.0 allows you to initiate a signature session using a notific * `Notification-Based flow` * The user receives a notification on their Smart-ID app to complete the signing process. * Suitable for scenarios where the user's identity or device is already known. - * Uses different interaction types compared to dynamic link flows. + * Uses different interactionDeprecated types compared to dynamic link flows. * `Dynamic Link flow` * Generates a dynamic link that the user must access to initiate the signing process. * Useful when the user's identity or device is not known beforehand. @@ -1274,9 +1347,9 @@ The request parameters for the notification-based signature session are as follo * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. -* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. +* `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. + * Each interactionDeprecated object includes: + * `type`: Required. Type of interactionDeprecated. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. * `requestProperties`: requestProperties: @@ -1365,7 +1438,7 @@ System.out.println("Verification Code Value: " + verificationCode.getValue()); ``` ## Examples of Allowed Interactions Order -In notification-based flows, the available interaction types differ from those in dynamic link flows. Below are the interaction types allowed in notification-based flows: +In notification-based flows, the available interactionDeprecated types differ from those in dynamic link flows. Below are the interactionDeprecated types allowed in notification-based flows: * `verificationCodeChoice` with `displayText60` * `confirmationMessageAndVerificationCodeChoice` with `displayText200` @@ -1421,7 +1494,7 @@ try { } catch (RelyingPartyAccountConfigurationException e) { System.out.println("Relying party account configuration issue."); } catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interaction is not supported by the user's app."); + System.out.println("The required interactionDeprecated is not supported by the user's app."); } catch (ServerMaintenanceException e) { System.out.println("Server maintenance in progress, please try again later."); } catch (SmartIdClientException e) { @@ -1430,7 +1503,7 @@ try { ``` ## Additional Information -* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interaction it supports from the list. For notification-based flows, use `verificationCodeChoice` and `confirmationMessageAndVerificationCodeChoice`. +* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. For notification-based flows, use `verificationCodeChoice` and `confirmationMessageAndVerificationCodeChoice`. ```java builder.withAllowedInteractionsOrder(List.of( Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm the transaction of 1024.50 EUR"), diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java similarity index 88% rename from src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java rename to src/main/java/ee/sk/smartid/AuthenticationIdentity.java index 13b844a4..ab16a0a7 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -61,27 +61,6 @@ public void setSurname(String surname) { this.surname = surname; } - /** - * Instead use: - * {@link #getSurname()} - * @return surname of the person - */ - @Deprecated - public String getSurName() { - return surname; - } - - /** - * @param surName surname - *

      - * Instead use: - * {@link #setSurname(String)} - */ - @Deprecated - public void setSurName(String surName) { - this.surname = surName; - } - public String getIdentityNumber() { return identityNumber; } diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java new file mode 100644 index 00000000..6aa0a863 --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java @@ -0,0 +1,62 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.bouncycastle.asn1.x500.style.BCStyle; + +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.NationalIdentityNumberUtil; + +public class AuthenticationIdentityMapper { + + /** + * Maps the X509 certificate to an {@link AuthenticationIdentity} object. + * + * @param certificate Certificate to be converted to an {@link AuthenticationIdentity} object + * @return AuthenticationIdentity object + */ + public static AuthenticationIdentity from(X509Certificate certificate) { + var identity = new AuthenticationIdentity(certificate); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) + .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); + identity.setDateOfBirth(getDateOfBirth(identity)); + return identity; + } + + private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { + return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) + .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); + } +} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java similarity index 98% rename from src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java rename to src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java index 2292eedc..f1faadf5 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java similarity index 98% rename from src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java rename to src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index 1283e92c..ce0aa48f 100644 --- a/src/main/java/ee/sk/smartid/v2/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.util; +package ee.sk.smartid.util; /*- * #%L @@ -52,7 +52,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; public class CertificateAttributeUtil { private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); diff --git a/src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java similarity index 98% rename from src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java rename to src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index 1faf0a19..f1f77da8 100644 --- a/src/main/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.util; +package ee.sk.smartid.util; /*- * #%L @@ -26,7 +26,7 @@ * #L% */ -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java index b45809cb..382ee85e 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java @@ -40,7 +40,7 @@ import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionResult; import ee.sk.smartid.v2.rest.dao.SessionSignature; diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java index e7ba7402..281d87ba 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java @@ -42,23 +42,20 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.time.LocalDate; import java.util.ArrayList; import java.util.Base64; import java.util.Date; import java.util.Enumeration; import java.util.List; -import java.util.Optional; -import org.bouncycastle.asn1.x500.style.BCStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentityMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.v2.util.CertificateAttributeUtil; -import ee.sk.smartid.v2.util.NationalIdentityNumberUtil; import ee.sk.smartid.util.StringUtil; /** @@ -198,15 +195,7 @@ public void clearTrustedCACertificates() { } public static AuthenticationIdentity constructAuthenticationIdentity(X509Certificate certificate) { - AuthenticationIdentity identity = new AuthenticationIdentity(certificate); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) - .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); - identity.setDateOfBirth(getDateOfBirth(identity)); - return identity; + return AuthenticationIdentityMapper.from(certificate); } private void initializeTrustedCACertificatesFromKeyStore() { @@ -289,9 +278,4 @@ private static byte[] addPadding(byte[] digestInfoPrefix, byte[] digest) { System.arraycopy(digest, 0, digestWithPrefix, digestInfoPrefix.length, digest.length); return digestWithPrefix; } - - private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { - return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) - .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); - } } diff --git a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java index 0c04cceb..a0729df1 100644 --- a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java @@ -39,7 +39,7 @@ import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionResult; import ee.sk.smartid.v2.rest.dao.SessionStatus; diff --git a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java index 3a340d65..55f3981e 100644 --- a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java @@ -37,7 +37,7 @@ import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionSignature; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java index 330eb57e..263f9641 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java @@ -40,7 +40,7 @@ import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionResult; import ee.sk.smartid.v2.rest.SessionStatusPoller; import ee.sk.smartid.v2.rest.SmartIdConnector; diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java index b294d46a..77e577a3 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java @@ -31,7 +31,7 @@ import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java index b459695c..fb97bd90 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java @@ -38,7 +38,7 @@ import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SessionStatusRequest; import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; diff --git a/src/main/java/ee/sk/smartid/v3/AuthCode.java b/src/main/java/ee/sk/smartid/v3/AuthCode.java index 4435772a..049777c5 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthCode.java +++ b/src/main/java/ee/sk/smartid/v3/AuthCode.java @@ -34,6 +34,7 @@ import org.bouncycastle.crypto.params.KeyParameter; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; /** * This class is responsible for creating an authentication code hash for the dynamic link. @@ -50,12 +51,12 @@ private AuthCode() { * * @param dynamicLinkType the type of the dynamic link @{@link DynamicLinkType} * @param sessionType the type of the session @{@link SessionType} - * @param sessionSecret the session secret * @param elapsedSeconds the time from session creation response was received + * @param sessionSecret the session secret in Base64 format * @return the authentication code in Base64 URL safe format */ - public static String createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String sessionSecret, long elapsedSeconds) { - validateInputs(dynamicLinkType, sessionType, sessionSecret); + public static String createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, long elapsedSeconds, String sessionSecret) { + validateHashingInputs(dynamicLinkType, sessionType); String payload = createPayload(dynamicLinkType, sessionType, elapsedSeconds); return hashThePayload(payload, sessionSecret); } @@ -68,10 +69,12 @@ public static String createHash(DynamicLinkType dynamicLinkType, SessionType ses * @return the hashed payload in Base64 URL safe format */ public static String hashThePayload(String payload, String sessionSecret) { + validatePayloadInputs(payload, sessionSecret); HMac hmac = new HMac(new SHA256Digest()); - hmac.init(new KeyParameter(sessionSecret.getBytes(StandardCharsets.UTF_8))); + byte[] secret = Base64.getDecoder().decode(sessionSecret.getBytes(StandardCharsets.UTF_8)); + hmac.init(new KeyParameter(secret)); - byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + byte[] payloadBytes = payload.getBytes(StandardCharsets.US_ASCII); hmac.update(payloadBytes, 0, payloadBytes.length); byte[] result = new byte[hmac.getMacSize()]; @@ -80,14 +83,20 @@ public static String hashThePayload(String payload, String sessionSecret) { return Base64.getUrlEncoder().withoutPadding().encodeToString(result); } - private static void validateInputs(DynamicLinkType dynamicLinkType, SessionType sessionType, String sessionSecret) { + private static void validateHashingInputs(DynamicLinkType dynamicLinkType, SessionType sessionType) { if (dynamicLinkType == null) { throw new SmartIdClientException("Dynamic link type must be set"); } if (sessionType == null) { throw new SmartIdClientException("Session type must be set"); } - if (sessionSecret == null) { + } + + private static void validatePayloadInputs(String payload, String sessionSecret) { + if (StringUtil.isEmpty(payload)) { + throw new SmartIdClientException("Payload must be set"); + } + if (StringUtil.isEmpty(sessionSecret)) { throw new SmartIdClientException("Session secret must be set"); } } diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java index c34c4041..f15d1af8 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,5 +27,22 @@ */ public enum AuthenticationCertificateLevel { - ADVANCED, QUALIFIED + ADVANCED(1), + QUALIFIED(2); + + private final int level; + + AuthenticationCertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return the level of the certificate + */ + public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { + return this == certificateLevel || this.level > certificateLevel.level; + } } diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java new file mode 100644 index 00000000..772533d5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java @@ -0,0 +1,240 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static java.util.Arrays.asList; +import static org.slf4j.LoggerFactory.getLogger; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import org.slf4j.Logger; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentityMapper; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates authentication response and converts it to {@link AuthenticationIdentity} + */ +public class AuthenticationResponseValidator { + + private static final Logger logger = getLogger(AuthenticationResponseValidator.class); + + private final List trustedCACertificates = new ArrayList<>(); + + /** + * Initializes the mapper with trusted CA certificates from a keystore. + *

      + * Uses default values to initialize the keystore. + */ + public AuthenticationResponseValidator() { + initializeTrustedCACertificatesFromKeyStore("trusted_certificates.jks", "changeIt"); + } + + /** + * Initializes the mapper with trusted CA certificates from a keystore. + * + * @param truststorePath path to the keystore + * @param truststorePassword password for the keystore + */ + public AuthenticationResponseValidator(String truststorePath, String truststorePassword) { + initializeTrustedCACertificatesFromKeyStore(truststorePath, truststorePassword); + } + + /** + * Initializes the mapper with trusted CA certificates from the input + * + * @param trustedCertificates trusted CA certificates + */ + public AuthenticationResponseValidator(X509Certificate[] trustedCertificates) { + trustedCACertificates.addAll(asList(trustedCertificates)); + } + + /** + * Adds a trusted CA certificate to the mapper + * + * @param certificate trusted CA certificate + */ + public void addTrustedCACertificate(X509Certificate certificate) { + trustedCACertificates.add(certificate); + } + + /** + * Maps the Smart-ID authentication response {@link DynamicLinkAuthenticationResponse} to {@link AuthenticationIdentity} + *

      + * Uses {@link AuthenticationCertificateLevel#QUALIFIED} as the request certificate level + * + * @param dynamicLinkAuthenticationResponse Smart-ID authentication response + * @return authentication identity + */ + public AuthenticationIdentity toAuthenticationIdentity(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { + return toAuthenticationIdentity(dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, randomChallenge); + } + + /** + * Maps the Smart-ID authentication response {@link DynamicLinkAuthenticationResponse} to {@link AuthenticationIdentity} + * + * @param dynamicLinkAuthenticationResponse Smart-ID authentication response + * @param requestedCertificateLevel Certificate level used in the authentication session request + * @param randomChallenge Generate string used in the authentication session request + * @return authentication identity + */ + public AuthenticationIdentity toAuthenticationIdentity(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, + AuthenticationCertificateLevel requestedCertificateLevel, + String randomChallenge) { + validateInputs(dynamicLinkAuthenticationResponse, randomChallenge); + validateCertificate(dynamicLinkAuthenticationResponse, requestedCertificateLevel); + validateSignature(dynamicLinkAuthenticationResponse, randomChallenge); + return AuthenticationIdentityMapper.from(dynamicLinkAuthenticationResponse.getCertificate()); + } + + private void validateInputs(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { + if (dynamicLinkAuthenticationResponse == null) { + throw new SmartIdClientException("Dynamic link authentication response is not provided"); + } + if (StringUtil.isEmpty(randomChallenge)) { + throw new SmartIdClientException("Random challenge is not provided"); + } + } + + private void validateCertificate(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (dynamicLinkAuthenticationResponse.getCertificate() == null) { + throw new SmartIdClientException("Certificate is not provided"); + } + validateCertificateNotExpired(dynamicLinkAuthenticationResponse.getCertificate()); + validateCertificateIsTrusted(dynamicLinkAuthenticationResponse.getCertificate()); + validateCertificateLevel(dynamicLinkAuthenticationResponse, requestedCertificateLevel); + } + + private void validateSignature(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { + if (StringUtil.isEmpty(dynamicLinkAuthenticationResponse.getAlgorithmName())) { + throw new SmartIdClientException("Algorithm name is not provided"); + } + if (StringUtil.isEmpty(dynamicLinkAuthenticationResponse.getSignatureValueInBase64())) { + throw new SmartIdClientException("Signature value is not provided"); + } + try { + Signature signature = getSignature(dynamicLinkAuthenticationResponse); + signature.initVerify(dynamicLinkAuthenticationResponse.getCertificate().getPublicKey()); + String data = createSignatureData(dynamicLinkAuthenticationResponse, randomChallenge); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + byte[] signedHash = dynamicLinkAuthenticationResponse.getSignatureValue(); + if (!signature.verify(signedHash)) { + throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); + } + } catch (GeneralSecurityException ex) { + logger.error("Signature verification failed"); + throw new UnprocessableSmartIdResponseException("Signature verification failed", ex); + } + } + + private static Signature getSignature(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse) throws NoSuchAlgorithmException { + String algorithm = dynamicLinkAuthenticationResponse.getAlgorithmName().replace("Encryption", ""); + try { + return Signature.getInstance(algorithm); + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", algorithm); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } + } + + private void validateCertificateLevel(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (requestedCertificateLevel == null) { + return; + } + if (dynamicLinkAuthenticationResponse.getCertificateLevel() == null) { + throw new SmartIdClientException("Certificate level is not provided"); + } + if (!dynamicLinkAuthenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private void validateCertificateIsTrusted(X509Certificate responseCertificate) { + for (X509Certificate trustedCACertificate : trustedCACertificates) { + try { + responseCertificate.verify(trustedCACertificate.getPublicKey()); + logger.info("Certificate verification passed for '{}' against CA responseCertificate '{}' ", + responseCertificate.getSubjectX500Principal(), + trustedCACertificate.getSubjectX500Principal()); + return; + } catch (GeneralSecurityException ex) { + logger.debug("Error verifying signer's responseCertificate: {} against CA responseCertificate: {}", + responseCertificate.getSubjectX500Principal(), + trustedCACertificate.getSubjectX500Principal(), ex); + } + } + throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); + } + + private void initializeTrustedCACertificatesFromKeyStore(String truststorePath, String truststorePassword) { + try (InputStream is = AuthenticationResponseValidator.class.getResourceAsStream(truststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, truststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + addTrustedCACertificate(certificate); + } + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing trusted CA certificates", e); + throw new SmartIdClientException("Error initializing trusted CA certificates", e); + } + } + + private static void validateCertificateNotExpired(X509Certificate certificate) { + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + throw new UnprocessableSmartIdResponseException("Signer's certificate is not valid", ex); + } + } + + private static String createSignatureData(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { + return String.format("%s;%s;%s", SignatureProtocol.ACSP_V1.name(), + dynamicLinkAuthenticationResponse.getServerRandom(), + randomChallenge); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java new file mode 100644 index 00000000..685f98a2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java @@ -0,0 +1,147 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Represents the authentication response after a successful authentication sessions status response was received. + * + *

      Use with {@link AuthenticationResponseValidator} to validate the certificate and the signature. + */ +public class DynamicLinkAuthenticationResponse { + + private String endResult; + private String serverRandom; + private HashType hashType; + private String signatureValueInBase64; + private String algorithmName; + private X509Certificate certificate; + private AuthenticationCertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; + private String deviceIpAddress; + + public String getEndResult() { + return endResult; + } + + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + /** + * Decodes Base64 encoded signature value and returns it as a byte array. + * + * @return signature value as a byte array + */ + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64.getBytes(StandardCharsets.UTF_8)); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + public String getAlgorithmName() { + return algorithmName; + } + + public void setAlgorithmName(String algorithmName) { + this.algorithmName = algorithmName; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + public AuthenticationCertificateLevel getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public HashType getHashType() { + return hashType; + } + + public void setHashType(HashType hashType) { + this.hashType = hashType; + } + + public String getDocumentNumber() { + return documentNumber; + } + + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + + public String getServerRandom() { + return serverRandom; + } + + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java new file mode 100644 index 00000000..7737a505 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java @@ -0,0 +1,163 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +/** + * Validates and maps the session status received as dynamic-link authentication response + */ +public class DynamicLinkAuthenticationResponseMapper { + + private static final Logger logger = LoggerFactory.getLogger(DynamicLinkAuthenticationResponseMapper.class); + + /** + * Maps session status to dynamic-link authentication response + * + * @param sessionStatus session status received from Smart-ID server + * @return dynamic-link authentication response + */ + public static DynamicLinkAuthenticationResponse from(SessionStatus sessionStatus) { + validateSessionStatus(sessionStatus); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setEndResult(sessionResult.getEndResult()); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + dynamicLinkAuthenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + dynamicLinkAuthenticationResponse.setCertificate(toCertificate(sessionCertificate)); + dynamicLinkAuthenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); + dynamicLinkAuthenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + dynamicLinkAuthenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + dynamicLinkAuthenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + dynamicLinkAuthenticationResponse.setServerRandom(sessionSignature.getServerRandom()); + return dynamicLinkAuthenticationResponse; + } + + private static void validateSessionStatus(SessionStatus sessionStatus) { + if (sessionStatus == null) { + throw new SmartIdClientException("Session status parameter is not provided"); + } + + validateResult(sessionStatus.getResult()); + validateSignatureProtocol(sessionStatus); + validateSignature(sessionStatus.getSignature()); + validateCertificate(sessionStatus.getCert()); + + if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { + throw new UnprocessableSmartIdResponseException("Interaction flow used parameter is missing in the session status"); + } + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Session result parameter is missing"); + } + + validateEndResult(sessionResult.getEndResult()); + + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); + } + } + + private static void validateEndResult(String endResult) { + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); + } + if (!"OK".equalsIgnoreCase(endResult)) { + ErrorResultHandler.handle(endResult); + } + } + + private static void validateSignatureProtocol(SessionStatus sessionStatus) { + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + logger.error("Signature protocol parameter is missing in session status"); + throw new UnprocessableSmartIdResponseException("Signature protocol parameter is missing in session status"); + } + + if (!SignatureProtocol.ACSP_V1.name().equals(sessionStatus.getSignatureProtocol())) { + logger.error("Invalid signature protocol in sessions status: {}", sessionStatus.getSignatureProtocol()); + throw new UnprocessableSmartIdResponseException("Invalid signature protocol in sessions status"); + } + } + + private static void validateSignature(SessionSignature sessionSignature) { + if (sessionSignature == null) { + throw new UnprocessableSmartIdResponseException("Signature parameter is missing in session status"); + } + + if (StringUtil.isEmpty(sessionSignature.getValue())) { + throw new UnprocessableSmartIdResponseException("Value parameter is missing in signature"); + } + + if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { + throw new UnprocessableSmartIdResponseException("Server random parameter is missing in signature"); + } + + if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature algorithm parameter is missing in signature"); + } + } + + private static void validateCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); + } + + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); + } + } + + private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { + return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + } + + private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { + return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java index 3896f594..697cf946 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -26,22 +26,22 @@ * #L% */ +import java.util.Base64; import java.util.List; -import java.util.Optional; import java.util.Set; -import java.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; /** * Class for building a dynamic link authentication session request @@ -50,18 +50,15 @@ public class DynamicLinkAuthenticationSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(DynamicLinkAuthenticationSessionRequestBuilder.class); - private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = - Set.of(InteractionFlow.VERIFICATION_CODE_CHOICE, InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - private final SmartIdConnector connector; private String relyingPartyUUID; private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel; + private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; private String randomChallenge; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; private String nonce; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private Set capabilities; private SemanticsIdentifier semanticsIdentifier; @@ -100,6 +97,8 @@ public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyName(Strin /** * Sets the certificate level + *

      + * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} * * @param certificateLevel the certificate level * @return this builder @@ -150,7 +149,7 @@ public DynamicLinkAuthenticationSessionRequestBuilder withNonce(String nonce) { * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ - public DynamicLinkAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + public DynamicLinkAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; return this; } @@ -292,14 +291,7 @@ private void validateAllowedInteractionOrder() { logger.error("Parameter allowedInteractionsOrder must be set"); throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); } - Optional notSupportedInteraction = allowedInteractionsOrder.stream() - .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) - .findFirst(); - if (notSupportedInteraction.isPresent()) { - logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); - throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); - } - allowedInteractionsOrder.forEach(Interaction::validate); + allowedInteractionsOrder.forEach(DynamicLinkInteraction::validate); } private AuthenticationSessionRequest createAuthenticationRequest() { diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 22c59584..96ad72ec 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -36,6 +36,7 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; import ee.sk.smartid.v3.rest.dao.RequestProperties; public class DynamicLinkCertificateChoiceSessionRequestBuilder { diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java index 58ed2293..be83270e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java @@ -27,7 +27,6 @@ */ import java.util.List; -import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -35,21 +34,20 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; public class DynamicLinkSignatureSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(DynamicLinkSignatureSessionRequestBuilder.class); - private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = - Set.of(InteractionFlow.VERIFICATION_CODE_CHOICE, InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - private final SmartIdConnector connector; private String relyingPartyUUID; @@ -59,7 +57,7 @@ public class DynamicLinkSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private SignableData signableData; @@ -158,7 +156,7 @@ public DynamicLinkSignatureSessionRequestBuilder withCapabilities(String... capa * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; return this; } @@ -305,13 +303,6 @@ private void validateAllowedInteractions() { if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); } - Optional notSupportedInteraction = allowedInteractionsOrder.stream() - .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) - .findFirst(); - if (notSupportedInteraction.isPresent()) { - logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); - throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); - } allowedInteractionsOrder.forEach(Interaction::validate); } diff --git a/src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java new file mode 100644 index 00000000..7c50c837 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java @@ -0,0 +1,66 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; + +/** + * Handles session status results that end as completed but with an error + */ +public class ErrorResultHandler { + + public static void handle(String endResult) { + if (endResult == null) { + throw new SmartIdClientException("Session end result is not provided"); + } + + switch (endResult.toUpperCase()) { + case "USER_REFUSED" -> throw new UserRefusedException(); + case "TIMEOUT" -> throw new SessionTimeoutException(); + case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); + case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); + case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); + case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); + case "USER_REFUSED_DISPLAYTEXTANDPIN" -> throw new UserRefusedDisplayTextAndPinException(); + case "USER_REFUSED_VC_CHOICE" -> throw new UserRefusedVerificationChoiceException(); + case "USER_REFUSED_CONFIRMATIONMESSAGE" -> throw new UserRefusedConfirmationMessageException(); + case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + endResult); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java index 365d22b9..a4ff7725 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java @@ -26,23 +26,24 @@ * #L% */ +import java.util.Base64; import java.util.List; -import java.util.Optional; import java.util.Set; -import java.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.VerificationCode; /** @@ -52,9 +53,6 @@ public class NotificationAuthenticationSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(NotificationAuthenticationSessionRequestBuilder.class); - private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = - Set.of(InteractionFlow.DISPLAY_TEXT_AND_PIN, InteractionFlow.CONFIRMATION_MESSAGE); - private final SmartIdConnector connector; private String relyingPartyUUID; @@ -63,7 +61,7 @@ public class NotificationAuthenticationSessionRequestBuilder { private String randomChallenge; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; private String nonce; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private Set capabilities; private SemanticsIdentifier semanticsIdentifier; @@ -152,7 +150,7 @@ public NotificationAuthenticationSessionRequestBuilder withNonce(String nonce) { * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ - public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; return this; } @@ -293,13 +291,6 @@ private void validateAllowedInteractionOrder() { logger.error("Parameter allowedInteractionsOrder must be set"); throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); } - Optional notSupportedInteraction = allowedInteractionsOrder.stream() - .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) - .findFirst(); - if (notSupportedInteraction.isPresent()) { - logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); - throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); - } allowedInteractionsOrder.forEach(Interaction::validate); } diff --git a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java index 6b07eb63..6ebeb1b5 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java @@ -31,11 +31,12 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import java.util.Set; diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java index 0fb17eb4..ad69346c 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java @@ -27,7 +27,6 @@ */ import java.util.List; -import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -35,12 +34,14 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.InteractionFlow; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.VerificationCode; @@ -48,9 +49,6 @@ public class NotificationSignatureSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); - private static final Set NOT_SUPPORTED_INTERACTION_FLOWS = - Set.of(InteractionFlow.DISPLAY_TEXT_AND_PIN, InteractionFlow.CONFIRMATION_MESSAGE); - private final SmartIdConnector connector; private String relyingPartyUUID; @@ -60,7 +58,7 @@ public class NotificationSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private SignableData signableData; @@ -158,7 +156,7 @@ public NotificationSignatureSessionRequestBuilder withCapabilities(Set c * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ - public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; return this; } @@ -289,13 +287,6 @@ private void validateAllowedInteractions() { if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); } - Optional notSupportedInteraction = allowedInteractionsOrder.stream() - .filter(interaction -> NOT_SUPPORTED_INTERACTION_FLOWS.contains(interaction.getType())) - .findFirst(); - if (notSupportedInteraction.isPresent()) { - logger.error("AllowedInteractionsOrder contains not supported interaction {}", notSupportedInteraction.get().getType()); - throw new SmartIdClientException("AllowedInteractionsOrder contains not supported interaction " + notSupportedInteraction.get().getType()); - } allowedInteractionsOrder.forEach(Interaction::validate); } diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java new file mode 100644 index 00000000..fd49755c --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java @@ -0,0 +1,179 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +public class SignatureResponseMapper { + + private static final Logger logger = LoggerFactory.getLogger(SignatureResponseMapper.class); + + /** + * Create {@link SingatureResponse} from {@link SessionStatus} + * + * @param sessionStatus session status response + * @param requestedCertificateLevel certificate level used to start the signature session + * @param digest data that was sent for signing, will be used to validate signature + * @return the signature response + * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. + * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame + * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code + * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. + */ + public static SingatureResponse from(SessionStatus sessionStatus, + String requestedCertificateLevel, + String digest + ) throws UserRefusedException, + UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + validateSessionsStatus(sessionStatus, requestedCertificateLevel, digest); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate certificate = sessionStatus.getCert(); + + var singatureResponse = new SingatureResponse(); + singatureResponse.setEndResult(sessionResult.getEndResult()); + singatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); + singatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + singatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + singatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); + singatureResponse.setCertificateLevel(certificate.getCertificateLevel()); + singatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + singatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + singatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return singatureResponse; + } + + private static void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest) { + if (sessionStatus == null) { + throw new UnprocessableSmartIdResponseException("Session status is null"); + } + validateSessionResult(sessionStatus, requestedCertificateLevel, expectedDigest); + } + + private static void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest) { + SessionResult sessionResult = sessionStatus.getResult(); + + if (sessionResult == null) { + logger.error("Result is missing in the session status response"); + throw new SmartIdClientException("Result is missing in the session status response"); + } + + String endResult = sessionResult.getEndResult(); + if ("OK".equalsIgnoreCase(endResult)) { + logger.info("Session completed successfully"); + + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + logger.error("Document number is missing in the session result"); + throw new SmartIdClientException("Document number is missing in the session result"); + } + + if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { + logger.error("InteractionFlowUsed is missing in the session status"); + throw new SmartIdClientException("InteractionFlowUsed is missing in the session status"); + } + + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + validateSignature(sessionStatus, expectedDigest); + } else { + ErrorResultHandler.handle(endResult); + } + } + + private static void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { + if (sessionCertificate == null || sessionCertificate.getValue() == null) { + throw new SmartIdClientException("Missing certificate in session response"); + } + + try { + X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + cert.checkValidity(); + + if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { + throw new CertificateLevelMismatchException(); + } + + } catch (Exception e) { + throw new SmartIdClientException("Certificate validation failed", e); + } + } + + private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { + CertificateLevel requestedLevelEnum = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); + CertificateLevel returnedLevelEnum = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); + + // TODO - 03.12.24: does not validate if returned certificate level is same or higher as requested + return requestedLevelEnum == CertificateLevel.QSCD ? returnedLevelEnum == CertificateLevel.QUALIFIED : requestedLevelEnum == returnedLevelEnum; + } + + private static void validateSignature(SessionStatus sessionStatus, String expectedDigest) { + String signatureProtocol = sessionStatus.getSignatureProtocol(); + + if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { + validateRawDigestSignature(sessionStatus, expectedDigest); + } else { + throw new SmartIdClientException("Unknown signature protocol: " + signatureProtocol); + } + } + + private static void validateRawDigestSignature(SessionStatus sessionStatus, String expectedDigest) { + String signatureValue = sessionStatus.getSignature().getValue(); + String signatureAlgorithm = sessionStatus.getSignature().getSignatureAlgorithm(); + + if (!expectedDigest.equals(signatureValue)) { // TODO - 10.12.24: fix this, validating signature should be like in AuthenticationResponseMapper.validateSignature + throw new SmartIdClientException("RAW_DIGEST_SIGNATURE validation failed. Expected: " + expectedDigest + + ", but got: " + signatureValue); + } + + List allowedSignatureAlgorithms = Arrays.asList("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); + if (!allowedSignatureAlgorithms.contains(signatureAlgorithm)) { + throw new SmartIdClientException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signatureAlgorithm); + } + + logger.info("RAW_DIGEST_SIGNATURE successfully validated."); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v3/SingatureResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java rename to src/main/java/ee/sk/smartid/v3/SingatureResponse.java index 0b3c34a6..a3a091f0 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdAuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/v3/SingatureResponse.java @@ -33,7 +33,7 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -public class SmartIdAuthenticationResponse implements Serializable { +public class SingatureResponse implements Serializable { private String endResult; private String signedHashInBase64; diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index dcacb304..1975c795 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -65,13 +65,17 @@ public class SmartIdClient { private SmartIdConnector connector; private SSLContext trustSslContext; + private SessionStatusPoller sessionStatusPoller; + /** * Creates a new builder for creating a dynamic link certificate choice session request. * - * @return a builder for creating a new dynamic link certificate choice session request + * @return a builder for creating a new dynamic link certificate choice session request */ public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertificateRequest() { - return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()); + return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); } /** @@ -91,7 +95,9 @@ public NotificationCertificateChoiceSessionRequestBuilder createNotificationCert * @return builder for creating a new dynamic link authentication session request */ public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentication() { - return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()); + return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); } /** @@ -111,7 +117,9 @@ public NotificationAuthenticationSessionRequestBuilder createNotificationAuthent * @return builder for creating a new dynamic link signature session request */ public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { - return new DynamicLinkSignatureSessionRequestBuilder(getSmartIdConnector()); + return new DynamicLinkSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); } /** @@ -120,17 +128,21 @@ public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { * @return builder for creating a new notification signature session request */ public NotificationSignatureSessionRequestBuilder createNotificationSignature() { - return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()); + return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); } /** - * Create a new Smart-ID session status poller + * Returns the session status poller or creates a new one if it doesn't exist * * @return Sessions status poller */ - public SessionStatusPoller createSessionStatusPoller() { - var sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + public SessionStatusPoller getSessionsStatusPoller() { + if (sessionStatusPoller == null) { + sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + } return sessionStatusPoller; } @@ -252,6 +264,11 @@ public void setPollingSleepTimeout(TimeUnit unit, long timeout) { pollingSleepTimeout = timeout; } + /** + * Get smart-id connector. If connector is not set, then new will be created + * + * @return smart-id connector + */ public SmartIdConnector getSmartIdConnector() { if (null == connector) { Client client = configuredClient != null ? configuredClient : createClient(); diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java b/src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java deleted file mode 100644 index 2cddbdb5..00000000 --- a/src/main/java/ee/sk/smartid/v3/SmartIdRequestBuilderService.java +++ /dev/null @@ -1,260 +0,0 @@ -package ee.sk.smartid.v3; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; - -public class SmartIdRequestBuilderService { - - private static final Logger logger = LoggerFactory.getLogger(SmartIdRequestBuilderService.class); - - protected SignableHash hashToSign; - protected SignableData dataToSign; - protected String relyingPartyUUID; - protected String relyingPartyName; - protected SemanticsIdentifier semanticsIdentifier; - - protected String documentNumber; - protected String certificateLevel; - protected String nonce; - protected Set capabilities; - protected List allowedInteractionsOrder; - - /** - * Create {@link SmartIdAuthenticationResponse} from {@link SessionStatus} - * - * @param sessionStatus session status response - * @return the authentication response - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - */ - public SmartIdAuthenticationResponse createSmartIdAuthenticationResponse(SessionStatus sessionStatus, String requestedCertificateLevel, - String expectedDigest, String randomChallenge) throws UserRefusedException, - UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - validateAuthenticationResponse(sessionStatus, requestedCertificateLevel, expectedDigest, randomChallenge); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate certificate = sessionStatus.getCert(); - - var authenticationResponse = new SmartIdAuthenticationResponse(); - authenticationResponse.setEndResult(sessionResult.getEndResult()); - authenticationResponse.setSignedHashInBase64(getHashInBase64()); - authenticationResponse.setHashType(getHashType()); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - authenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - authenticationResponse.setRequestedCertificateLevel(getCertificateLevel()); - authenticationResponse.setCertificateLevel(certificate.getCertificateLevel()); - authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - authenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return authenticationResponse; - } - - private void validateAuthenticationResponse(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest, String randomChallenge) { - if (sessionStatus == null) { - throw new UnprocessableSmartIdResponseException("Session status is null"); - } - validateSessionResult(sessionStatus, requestedCertificateLevel, expectedDigest, randomChallenge); - } - - public void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest, String randomChallenge) { - SessionResult sessionResult = sessionStatus.getResult(); - - if (sessionResult == null) { - logger.error("Result is missing in the session status response"); - throw new SmartIdClientException("Result is missing in the session status response"); - } - - String endResult = sessionResult.getEndResult(); - if ("OK".equalsIgnoreCase(endResult)) { - logger.info("Session completed successfully"); - - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - logger.error("Document number is missing in the session result"); - throw new SmartIdClientException("Document number is missing in the session result"); - } - - if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { - logger.error("InteractionFlowUsed is missing in the session status"); - throw new SmartIdClientException("InteractionFlowUsed is missing in the session status"); - } - - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - validateSignature(sessionStatus, expectedDigest, randomChallenge); - } else { - handleSessionEndResultErrors(endResult); - } - } - - protected HashType getHashType() { - if (hashToSign != null) { - return hashToSign.getHashType(); - } - return dataToSign.getHashType(); - } - - protected String getHashInBase64() { - if (hashToSign != null) { - return hashToSign.getHashInBase64(); - } - return dataToSign.calculateHashInBase64(); - } - - protected String getCertificateLevel() { - return certificateLevel; - } - - private void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { - if (sessionCertificate == null || sessionCertificate.getValue() == null) { - throw new SmartIdClientException("Missing certificate in session response"); - } - - try { - X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - cert.checkValidity(); - - if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { - throw new CertificateLevelMismatchException(); - } - - } catch (Exception e) { - throw new SmartIdClientException("Certificate validation failed", e); - } - } - - private boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { - CertificateLevel requestedLevelEnum = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); - CertificateLevel returnedLevelEnum = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); - - return requestedLevelEnum == CertificateLevel.QSCD ? returnedLevelEnum == CertificateLevel.QUALIFIED : requestedLevelEnum == returnedLevelEnum; - } - - private void validateSignature(SessionStatus sessionStatus, String expectedDigest, String randomChallenge) { - String signatureProtocol = sessionStatus.getSignatureProtocol(); - - if (SignatureProtocol.ACSP_V1.name().equalsIgnoreCase(signatureProtocol)) { - validateAcspV1Signature(sessionStatus, randomChallenge); - } else if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { - validateRawDigestSignature(sessionStatus, expectedDigest); - } else { - throw new SmartIdClientException("Unknown signature protocol: " + signatureProtocol); - } - } - - private void validateAcspV1Signature(SessionStatus sessionStatus, String randomChallenge) { - String signatureValue = sessionStatus.getSignature().getValue(); - String dataToHash = sessionStatus.getSignatureProtocol() + ";" + - Base64.getEncoder().encodeToString(sessionStatus.getSignature().getServerRandom().getBytes(StandardCharsets.UTF_8)) + ";" + - Base64.getEncoder().encodeToString(randomChallenge.getBytes(StandardCharsets.UTF_8)); - - try { - MessageDigest digest = MessageDigest.getInstance(sessionStatus.getSignature().getSignatureAlgorithmParameters().getHashAlgorithm()); - byte[] hashedData = digest.digest(dataToHash.getBytes(StandardCharsets.UTF_8)); - String expectedSignature = Base64.getEncoder().encodeToString(hashedData); - - if (!expectedSignature.equals(signatureValue)) { - throw new SmartIdClientException("ACSP_V1 signature validation failed. Expected: " + expectedSignature - + ", but got: " + signatureValue); - } - } catch (NoSuchAlgorithmException ex) { - throw new SmartIdClientException("Error while creating digest for ACSP_V1 signature validation", ex); - } - - logger.info("ACSP_V1 signature successfully validated."); - } - - private void validateRawDigestSignature(SessionStatus sessionStatus, String expectedDigest) { - String signatureValue = sessionStatus.getSignature().getValue(); - String signatureAlgorithm = sessionStatus.getSignature().getSignatureAlgorithm(); - - if (!expectedDigest.equals(signatureValue)) { - throw new SmartIdClientException("RAW_DIGEST_SIGNATURE validation failed. Expected: " + expectedDigest - + ", but got: " + signatureValue); - } - - List allowedSignatureAlgorithms = Arrays.asList("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); - if (!allowedSignatureAlgorithms.contains(signatureAlgorithm)) { - throw new SmartIdClientException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signatureAlgorithm); - } - - logger.info("RAW_DIGEST_SIGNATURE successfully validated."); - } - - private void handleSessionEndResultErrors(String endResult) { - switch (endResult.toUpperCase()) { - case "USER_REFUSED" -> throw new UserRefusedException(); - case "TIMEOUT" -> throw new SessionTimeoutException(); - case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); - case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); - case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); - case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); - case "USER_REFUSED_DISPLAYTEXTANDPIN" -> throw new UserRefusedDisplayTextAndPinException(); - case "USER_REFUSED_VC_CHOICE" -> throw new UserRefusedVerificationChoiceException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE" -> throw new UserRefusedConfirmationMessageException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); - default -> throw new SmartIdClientException("Unexpected session result: " + endResult); - } - } -} diff --git a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java index bf9c6351..4d5d3d23 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,6 +34,9 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.v3.rest.dao.SessionStatus; +/** + * Provides methods for querying sessions status and polling session status + */ public class SessionStatusPoller { private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); @@ -45,6 +48,12 @@ public SessionStatusPoller(SmartIdConnector connector) { this.connector = connector; } + /** + * Loops session status query until state is COMPLETE + * + * @param sessionId session id from init session response + * @return Sessions status + */ public SessionStatus fetchFinalSessionStatus(String sessionId) { logger.debug("Starting to poll session status for session {}", sessionId); try { @@ -58,7 +67,7 @@ public SessionStatus fetchFinalSessionStatus(String sessionId) { private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { SessionStatus sessionStatus = null; while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - sessionStatus = pollSessionStatus(sessionId); + sessionStatus = getSessionsStatus(sessionId); if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { break; } @@ -69,11 +78,23 @@ private SessionStatus pollForFinalSessionStatus(String sessionId) throws Interru return sessionStatus; } - private SessionStatus pollSessionStatus(String sessionId) { - logger.debug("Polling session status"); + /** + * Query session status + * + * @param sessionId session id from init session response + * @return Sessions status + */ + public SessionStatus getSessionsStatus(String sessionId) { + logger.debug("Querying session status"); return connector.getSessionStatus(sessionId); } + /** + * Set polling sleep time + * + * @param unit time unit {@link TimeUnit} + * @param timeout time + */ public void setPollingSleepTime(TimeUnit unit, long timeout) { logger.debug("Setting polling sleep time to {} {}", timeout, unit); this.pollingSleepTimeUnit = unit; diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index 95932178..b0242d71 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -32,15 +32,15 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v3.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; public interface SmartIdConnector extends Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index 6e007b74..bcc37a24 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -45,16 +45,16 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.LoggingFilter; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.SessionStatus; import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.ForbiddenException; diff --git a/src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java index 80d2d659..82e90787 100644 --- a/src/main/java/ee/sk/smartid/v3/AcspV1SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java index db3b75e1..be4f5a51 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java @@ -31,10 +31,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.SignatureProtocol; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.RequestProperties; public class AuthenticationSessionRequest implements Serializable { @@ -52,7 +49,7 @@ public class AuthenticationSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String nonce; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; @JsonInclude(JsonInclude.Include.NON_NULL) private RequestProperties requestProperties; @@ -104,11 +101,11 @@ public void setNonce(String nonce) { this.nonce = nonce; } - public List getAllowedInteractionsOrder() { + public List getAllowedInteractionsOrder() { return allowedInteractionsOrder; } - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { + public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java new file mode 100644 index 00000000..e90426a8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java @@ -0,0 +1,63 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.v3.rest.dao.DynamicLinkInteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.v3.rest.dao.DynamicLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; + +public class DynamicLinkInteraction extends Interaction { + + private DynamicLinkInteraction(DynamicLinkInteractionFlow type) { + this.type = type; + } + + public static DynamicLinkInteraction displayTextAndPIN(String displayText60) { + var interaction = new DynamicLinkInteraction(DISPLAY_TEXT_AND_PIN); + interaction.displayText60 = displayText60; + return interaction; + } + + public static DynamicLinkInteraction confirmationMessage(String displayText200) { + var interaction = new DynamicLinkInteraction(CONFIRMATION_MESSAGE); + interaction.displayText200 = displayText200; + return interaction; + } + + @Override + protected void validateInteractionsDisplayText60() { + if (getType() == DISPLAY_TEXT_AND_PIN) { + validateDisplayText60(); + } + } + + @Override + protected void validateInteractionsDisplayText200() { + if (getType() == CONFIRMATION_MESSAGE) { + validateDisplayText200(); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java similarity index 50% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java index 56ac9b0c..ee93b397 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,45 +26,25 @@ * #L% */ -import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonValue; -public class SemanticsIdentifier implements Serializable { +public enum DynamicLinkInteractionFlow implements InteractionFlow { - private final String identifier; + DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), + CONFIRMATION_MESSAGE("confirmationMessage"); - public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { - this.identifier = "" + identityType + countryCode + "-" + identityNumber; - } + private final String code; - public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { - this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; - } + DynamicLinkInteractionFlow(String code) { + this.code = code; + } - public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { - this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; - } - - public SemanticsIdentifier(String identifier) { - this.identifier = identifier; - } - - public String getIdentifier() { - return identifier; - } - - public enum IdentityType { - PAS, IDC, PNO - } - - public enum CountryCode { - EE, LT, LV - } - - @Override - public String toString() { - return "SemanticsIdentifier{" + - "identifier='" + identifier + '\'' + - '}'; - } + @JsonValue + public String getCode() { + return code; + } + public boolean is(String typeCodeString) { + return this.getCode().equals(typeCodeString); + } } diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java index 0d64b19e..32e19c58 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java index ee59df02..be725814 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,57 +26,22 @@ * #L% */ -import static ee.sk.smartid.v3.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.v3.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; -import static ee.sk.smartid.v3.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; -import static ee.sk.smartid.v3.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; - -import java.io.Serializable; - import com.fasterxml.jackson.annotation.JsonInclude; import ee.sk.smartid.exception.permanent.SmartIdClientException; @JsonInclude(JsonInclude.Include.NON_NULL) -public class Interaction implements Serializable { - - private InteractionFlow type; - - private String displayText60; - private String displayText200; - - private Interaction(InteractionFlow type) { - this.type = type; - } - - public static Interaction displayTextAndPIN(String displayText60) { - Interaction interaction = new Interaction(DISPLAY_TEXT_AND_PIN); - interaction.displayText60 = displayText60; - return interaction; - } - - public static Interaction verificationCodeChoice(String displayText60) { - Interaction interaction = new Interaction(VERIFICATION_CODE_CHOICE); - interaction.displayText60 = displayText60; - return interaction; - } +public abstract class Interaction { - public static Interaction confirmationMessage(String displayText200) { - Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE); - interaction.displayText200 = displayText200; - return interaction; - } + protected InteractionFlow type; - public static Interaction confirmationMessageAndVerificationCodeChoice(String displayText200) { - Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - interaction.displayText200 = displayText200; - return interaction; - } + protected String displayText60; + protected String displayText200; public InteractionFlow getType() { return type; } - public void setType(InteractionFlow type) { + public void setType(DynamicLinkInteractionFlow type) { this.type = type; } @@ -97,36 +62,35 @@ public void setDisplayText200(String displayText200) { } public void validate() { - validateDisplayText60(); - validateDisplayText200(); + validateInteractionsDisplayText60(); + validateInteractionsDisplayText200(); } - private void validateDisplayText60() { - if (getType() == VERIFICATION_CODE_CHOICE || getType() == DISPLAY_TEXT_AND_PIN) { - if (getDisplayText60() == null) { - throw new SmartIdClientException("displayText60 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText60().length() > 60) { - throw new SmartIdClientException("displayText60 must not be longer than 60 characters"); - } - if (getDisplayText200() != null) { - throw new SmartIdClientException("displayText200 must be null for AllowedInteractionOrder of type " + getType()); - } + protected abstract void validateInteractionsDisplayText60(); + + protected abstract void validateInteractionsDisplayText200(); + + protected void validateDisplayText60() { + if (getDisplayText60() == null) { + throw new SmartIdClientException("displayText60 cannot be null for AllowedInteractionOrder of type " + getType()); + } + if (getDisplayText60().length() > 60) { + throw new SmartIdClientException("displayText60 must not be longer than 60 characters"); + } + if (getDisplayText200() != null) { + throw new SmartIdClientException("displayText200 must be null for AllowedInteractionOrder of type " + getType()); } } - private void validateDisplayText200() { - if (getType() == CONFIRMATION_MESSAGE || getType() == CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { - if (getDisplayText200() == null) { - throw new SmartIdClientException("displayText200 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText200().length() > 200) { - throw new SmartIdClientException("displayText200 must not be longer than 200 characters"); - } - if (getDisplayText60() != null) { - throw new SmartIdClientException("displayText60 must be null for AllowedInteractionOrder of type " + getType()); - } + protected void validateDisplayText200() { + if (getDisplayText200() == null) { + throw new SmartIdClientException("displayText200 cannot be null for AllowedInteractionOrder of type " + getType()); + } + if (getDisplayText200().length() > 200) { + throw new SmartIdClientException("displayText200 must not be longer than 200 characters"); + } + if (getDisplayText60() != null) { + throw new SmartIdClientException("displayText60 must be null for AllowedInteractionOrder of type " + getType()); } } - } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java index 7e07ea57..d4e9f5c0 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2024 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,28 +26,9 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonValue; +public interface InteractionFlow { -public enum InteractionFlow { - - DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), - CONFIRMATION_MESSAGE("confirmationMessage"), - VERIFICATION_CODE_CHOICE("verificationCodeChoice"), - CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); - - private final String code; - - InteractionFlow(String code) { - this.code = code; - } - - @JsonValue - public String getCode() { - return code; - } - - public boolean is(String typeCodeString) { - return this.getCode().equals(typeCodeString); - } + String getCode(); + boolean is(String typeCodeString); } diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java similarity index 95% rename from src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java index 428bf95e..2d16b049 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L @@ -29,7 +29,6 @@ import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import ee.sk.smartid.v3.rest.dao.VerificationCode; @JsonIgnoreProperties(ignoreUnknown = true) public class NotificationAuthenticationSessionResponse implements Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java index b63eaa45..7a79f9f9 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java new file mode 100644 index 00000000..9096bbe0 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java @@ -0,0 +1,63 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.v3.rest.dao.NotificationInteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.v3.rest.dao.NotificationInteractionFlow.VERIFICATION_CODE_CHOICE; + +public class NotificationInteraction extends Interaction { + + public NotificationInteraction(NotificationInteractionFlow notificationInteractionFlow) { + this.type = notificationInteractionFlow; + } + + public static NotificationInteraction verificationCodeChoice(String displayText60) { + var interactionDeprecated = new NotificationInteraction(VERIFICATION_CODE_CHOICE); + interactionDeprecated.displayText60 = displayText60; + return interactionDeprecated; + } + + public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { + var interactionDeprecated = new NotificationInteraction(CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); + interactionDeprecated.displayText200 = displayText200; + return interactionDeprecated; + } + + @Override + protected void validateInteractionsDisplayText60() { + if (getType() == VERIFICATION_CODE_CHOICE) { + validateDisplayText60(); + } + } + + @Override + protected void validateInteractionsDisplayText200() { + if (getType() == CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { + validateDisplayText200(); + } + } +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java new file mode 100644 index 00000000..62d34d74 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java @@ -0,0 +1,50 @@ +package ee.sk.smartid.v3.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum NotificationInteractionFlow implements InteractionFlow { + + VERIFICATION_CODE_CHOICE("verificationCodeChoice"), + CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); + + private final String code; + + NotificationInteractionFlow(String code) { + this.code = code; + } + + @JsonValue + public String getCode() { + return code; + } + + public boolean is(String typeCodeString) { + return this.getCode().equals(typeCodeString); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java similarity index 95% rename from src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java index 8d86a9e8..93db3c88 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L @@ -29,7 +29,6 @@ import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import ee.sk.smartid.v3.rest.dao.VerificationCode; @JsonIgnoreProperties(ignoreUnknown = true) public class NotificationSignatureSessionResponse implements Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java index eb5cc822..0ffbf2e5 100644 --- a/src/main/java/ee/sk/smartid/v3/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid.v3.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java index bf919132..ca28ed2e 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java @@ -31,10 +31,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.SignatureProtocol; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.RequestProperties; public class SignatureSessionRequest implements Serializable { @@ -55,7 +52,7 @@ public class SignatureSessionRequest implements Serializable { private Set capabilities; @JsonInclude(JsonInclude.Include.NON_EMPTY) - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; @JsonInclude(JsonInclude.Include.NON_NULL) private RequestProperties requestProperties; @@ -112,11 +109,11 @@ public void setCapabilities(Set capabilities) { this.capabilities = capabilities; } - public List getAllowedInteractionsOrder() { + public List getAllowedInteractionsOrder() { return allowedInteractionsOrder; } - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { + public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; } diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java new file mode 100644 index 00000000..65960742 --- /dev/null +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java @@ -0,0 +1,66 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class AuthenticationIdentityMapperTest { + + private static final byte[] AUTH_CERT = FileUtil.readFileBytes("test-certs/auth-cert-40504040001.pem.crt"); + + @Test + void from() { + X509Certificate certificate = getX509Certificate(); + AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); + + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("40504040001", authenticationIdentity.getIdentityNumber()); + assertEquals("EE", authenticationIdentity.getCountry()); + + assertEquals(certificate, authenticationIdentity.getAuthCertificate()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + private static X509Certificate getX509Certificate() { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(AUTH_CERT)); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java index e828b845..e3afc41f 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeTest.java @@ -58,11 +58,11 @@ import org.slf4j.LoggerFactory; import ee.sk.smartid.v2.AuthenticationHash; -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.CertificateParser; import ee.sk.smartid.HashType; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.FileUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.SignableHash; diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java index e85c8df8..43aedbcd 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java @@ -43,9 +43,9 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.v2.AuthenticationHash; -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.FileUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.SignableData; diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java index 66735c9f..c757b008 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java @@ -32,28 +32,10 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; public class AuthenticationIdentityTest { - @SuppressWarnings("deprecation") - @Test - public void setSurName() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setSurName("surname1"); - - assertThat(authenticationIdentity.getSurname(), is("surname1")); - } - - @SuppressWarnings("deprecation") - @Test - public void getSurName() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setSurname("surname"); - - assertThat(authenticationIdentity.getSurName(), is("surname")); - } - @Test public void getIdentityCode() { AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java index c0d8f995..0b8387f1 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java @@ -59,7 +59,7 @@ import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionSignature; import ee.sk.smartid.v2.rest.dao.SessionStatus; diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java index 236ba145..6bf18586 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java @@ -49,6 +49,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.CertificateParser; import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; diff --git a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java index 25b0737b..78efcc71 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java @@ -56,7 +56,7 @@ import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionStatus; diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java index b1954f58..63ffd654 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java @@ -80,7 +80,7 @@ import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.SmartIdConnector; import ee.sk.smartid.v2.rest.SmartIdRestConnector; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java index 460612c3..486b6861 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java @@ -46,9 +46,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java index c1567833..d9b18389 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java @@ -31,8 +31,7 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java index 29ef95f5..60f66e69 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java @@ -59,7 +59,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.ClientRequestHeaderFilter; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index 9a66ee76..8ce94fd9 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -47,7 +47,7 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.v2.rest.dao.CertificateRequest; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; diff --git a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java index 97171e58..383916f2 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java @@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; public class SemanticsIdentifierTest { diff --git a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java index ee4fa7bd..35cc44fd 100644 --- a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java @@ -49,7 +49,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import ee.sk.smartid.v2.CertificateUtil; -import ee.sk.smartid.v2.util.CertificateAttributeUtil; +import ee.sk.smartid.util.CertificateAttributeUtil; public class CertificateAttributeUtilTest { diff --git a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java index 10acef0b..eceb9166 100644 --- a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java @@ -40,11 +40,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import ee.sk.smartid.v2.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.util.NationalIdentityNumberUtil; import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.v2.CertificateUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.v2.util.NationalIdentityNumberUtil; public class NationalIdentityNumberUtilTest { diff --git a/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java b/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java index 04aa1fc4..d3ead0cc 100644 --- a/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java +++ b/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.exception.permanent.SmartIdClientException; @@ -54,7 +55,7 @@ class AuthCodeTest { @ParameterizedTest @ArgumentsSource(AuthCodeArgumentsProvider.class) void createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String expectedPayload) { - String authCodeInBase64 = AuthCode.createHash(dynamicLinkType, sessionType, "sessionSecret", 1); + String authCodeInBase64 = AuthCode.createHash(dynamicLinkType, sessionType, 1, toBase64("sessionSecret")); String expected = hashThePayload(expectedPayload); assertEquals(expected, authCodeInBase64); @@ -62,26 +63,35 @@ void createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String @Test void createHash_dynamicLinkTypeNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(null, SessionType.AUTHENTICATION, "sessionSecret", 1)); + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(null, SessionType.AUTHENTICATION, 1, "sessionSecret")); assertEquals("Dynamic link type must be set", ex.getMessage()); } @Test void createHash_sessionTypeNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, null, "sessionSecret", 1)); + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, null, 1, "sessionSecret")); assertEquals("Session type must be set", ex.getMessage()); } - @Test - void createHash_sessionSecretNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, null, 1)); + @ParameterizedTest + @NullAndEmptySource + void createHash_payload_throwException(String payload) { + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.hashThePayload(payload, null)); + assertEquals("Payload must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createHash_sessionSecret_throwException(String sessionSecret) { + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.hashThePayload("payload", sessionSecret)); assertEquals("Session secret must be set", ex.getMessage()); } @ParameterizedTest @ValueSource(strings = {"QR.auth.1", "QR.sign.1", "QR.cert.1"}) void hashThePayload_validateUrlSafe(String payload) { - String authCodeHash = AuthCode.hashThePayload(payload, "sessionSecret"); + String sessionSecret = toBase64("sessionSecret"); + String authCodeHash = AuthCode.hashThePayload(payload, sessionSecret); String urlSafeBase64Pattern = "^[A-Za-z0-9_-]+={0,2}$"; assertTrue(authCodeHash.matches(urlSafeBase64Pattern)); assertEquals(hashThePayload(payload), authCodeHash); @@ -93,13 +103,17 @@ private String hashThePayload(String payload) { SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKeySpec); - byte[] data = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); + byte[] data = mac.doFinal(payload.getBytes(StandardCharsets.US_ASCII)); return Base64.getUrlEncoder().withoutPadding().encodeToString(data); } catch (InvalidKeyException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } } + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } + private static class AuthCodeArgumentsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java new file mode 100644 index 00000000..b683725b --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java @@ -0,0 +1,285 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Base64; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.HashType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; + +class AuthenticationResponseValidatorTest { + + private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + private static final String UNTRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); + + private AuthenticationResponseValidator authenticationResponseValidator; + + @BeforeEach + void setUp() { + authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); + } + + @Disabled("Do not have necessary test data to make this work.") + @Test + void toAuthenticationIdentity() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + dynamicLinkAuthenticationResponse.setEndResult("OK"); + + dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); + dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + + // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values + dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + + AuthenticationIdentity authenticationIdentity = + authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Disabled("Do not have necessary test data to make this work.") + @Test + void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + dynamicLinkAuthenticationResponse.setEndResult("OK"); + + dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); + dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + + // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values + dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + + AuthenticationIdentity authenticationIdentity = + authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, + AuthenticationCertificateLevel.ADVANCED, + "randomChallengeFromTestUserAuthRequest"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Disabled("Do not have necessary test data to make this work.") + @Test + void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidateCertificateLevel_ok() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + dynamicLinkAuthenticationResponse.setEndResult("OK"); + + dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); + dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + + // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values + dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + + AuthenticationIdentity authenticationIdentity = + authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, + null, + "randomChallengeFromTestUserAuthRequest"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Test + void toAuthenticationIdentity_dynamicLinkAuthenticationResponseIsMissing_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(null, null)); + + assertEquals("Dynamic link authentication response is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_randomChallengeIsNotProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, null)); + + assertEquals("Random challenge is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_certificateValueIsNotProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Certificate is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_expiredCertificateProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(EXPIRED_CERT)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Signer's certificate is not valid", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_certificateIsNotTrusted_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Signer's certificate is not trusted", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_certificateLevelIsNotProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(null); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Certificate level is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_certificateLevelIsLowerThanRequested_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.ADVANCED); + var exception = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Signer's certificate is below requested certificate level", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_algorithmNameIsNotProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Algorithm name is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_signatureValueIsNotProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Signature value is not provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_invalidAlgorithmNameIsProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("invalidAlgorithmName"); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Invalid signature algorithm was provided", exception.getMessage()); + } + + @Test + void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Signature verification failed", exception.getMessage()); + } + + @Disabled("Do not have necessary test data to make this work.") + @Test + void toAuthenticationIdentity_signatureDoesNotMatch_throwException() { + var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("bXhY5CO3gxQ2hxnuQm0Lm/4fXoFPogy4LwS6d0aUu9sZjCfNV5n6IUse45UYLhvmfK4NW5QarlYRTEqIYxlVQ0UMFm6WXQA5AHeOu/JoxKQDnbSeH8Y9FADOnqYXbPWz0W4aFVo0JFoMPO2JrwjC3rFrfded0EkD76vrazzwZxWNkWskC3jJq2Dgu3tsuDdv+Q4moNJYamADQtxYc7a16GNUEklo/ZlUS1pFanDplWTIwGaJd+ZWCvqPrz7cr+PObYfv4NsSN1QBij+eYDS+o6pTK/Ba/ve9AmdR4zS7dv/i1paSmGx3kbm/N0fNn+gelgPv8poOat1TGadT5FLEXWdytDW6I7S+d80xiInPHwKeXI4G4DL+F6zdRw8zWvR6ziXHIkxh/LnioRnoKxOiQZbQbrws2exjyFAS2HkX5UHugPfOkK0YSrJHVpwkOarDAvj7RoOHTFxLd/6FKbugDTG+0tIY4W6RROENePjZW+1eJIOkivO7/iHv3Qi6iIPhW9fB7XUDEtOdmmSrnheU6S9lvKnFYoW3Wcjy12bpK9QoaIzUykzQpO6maOxGr7nQv20AdM6y0vI16Y/8GIEqrGf9V/XVvv5SZFX3BPT3sAsBj0C18imfyyqhU33y1Gr/xMAc0Qbf4Cs92SLczY5yzd1BKGeM3ajaSaHRZbtjRdfiP7xyedyVyWF8COOHVfZb4cXwdpIbtXFkWNcYrfSnhLsRenhIrbKmiDsPRRZCZW8tpDWhr7ge2KY8wb1SbOa38WiNXTjNJAuviZ4ZmUOl5y4CrESdPXN7x7qH+jmfzxUSvBFYaSY2ey46ShHr9zQj7kz3NajIztGK7//sMnQsXuToUnSc5H0XwEwVUT6kSS6ZVYe58quDOgD47Dtj8wczXx081LSXAJXJ75XfxcwJhNn78oHVOR6EqTjOmRLlqj12Bw0WjhzIaut4wQdx0eTXGLqwn6b3RrVoVuwhJ2kwkURe0WDoKa7AWqYZBCHjGlgB3fNEBCNdKLw5ji+0C0jO")); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + + assertEquals("Failed to verify validity of signature returned by Smart-ID", exception.getMessage()); + } + + private X509Certificate toX509Certificate(String certificate) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java index 0afe6727..fe2c3fd3 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java @@ -34,7 +34,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Map; import java.util.stream.Collectors; @@ -62,7 +64,7 @@ void createUri_forDifferentDynamicLinks(DynamicLinkType dynamicLinkType) { .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(elapsedSeconds) - .withAuthCode(AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, "sessionSecret", elapsedSeconds)) + .withAuthCode(AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, elapsedSeconds, toBase64("sessionSecret"))) .createUri(); assertUri(uri, dynamicLinkType, SessionType.AUTHENTICATION); @@ -79,7 +81,7 @@ void createUri_withSessionType(SessionType sessionType) { .withSessionType(sessionType) .withSessionToken("sessionToken") .withElapsedSeconds(elapsedSeconds) - .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, "sessionSecret", elapsedSeconds)) + .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, elapsedSeconds, toBase64("sessionSecret"))) .createUri(); assertUri(uri, DynamicLinkType.QR_CODE, sessionType); @@ -190,7 +192,7 @@ void createQrCode_forDifferentSessionsTypes(SessionType sessionType) { .withSessionType(sessionType) .withSessionToken("sessionToken") .withElapsedSeconds(1L) - .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, "sessionSecret", 1)) + .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, 1, toBase64("sessionSecret"))) .createQrCodeDataUri(); String[] qrDataUriParts = qrDataUri.split(","); @@ -227,6 +229,10 @@ private static void assertUri(URI uri, DynamicLinkType qrCode, SessionType sessi assertThat(queryParams, hasEntry(equalTo("authCode"), matchesPattern("^[A-Za-z0-9_-]+={0,2}$"))); } + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } + private static Map toQueryParamsMap(URI uri) { return Arrays.stream(uri.getQuery().split("&")) .map(param -> param.split("=")) diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java new file mode 100644 index 00000000..93f4d666 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java @@ -0,0 +1,344 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +class DynamicLinkAuthenticationResponseMapperTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + @Test + void from() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", dynamicLinkAuthenticationResponse.getEndResult()); + assertEquals("signatureValue", dynamicLinkAuthenticationResponse.getSignatureValueInBase64()); + assertEquals(toX509Certificate(AUTH_CERT), dynamicLinkAuthenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, dynamicLinkAuthenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", dynamicLinkAuthenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", dynamicLinkAuthenticationResponse.getInteractionFlowUsed()); + assertEquals("0.0.0.0", dynamicLinkAuthenticationResponse.getDeviceIpAddress()); + } + + @Test + void from_sessionStatusNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> DynamicLinkAuthenticationResponseMapper.from(null)); + assertEquals("Session status parameter is not provided", exception.getMessage()); + } + + @Test + void from_sessionResultIsNotPresent_throwException() { + var sessionStatus = new SessionStatus(); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Session result parameter is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsNotPresent_throwException(String endResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("End result parameter is missing in the session result", exception.getMessage()); + } + + @Test + void from_endResultIsTimeout_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("TIMEOUT"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(SessionTimeoutException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @NullAndEmptySource + void from_documentNumberIsEmpty_throwException(String documentNumber) { + var sessionResult = toSessionResult(documentNumber); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Document number parameter is missing in the session result", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(signatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Signature protocol parameter is missing in session status", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) + void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(invalidSignatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid signature protocol in sessions status", exception.getMessage()); + } + + @Test + void from_signatureIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Signature parameter is missing in session status", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureValueIsNotProvided_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Value parameter is missing in signature", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_serverRandomIsNotProvided_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Server random parameter is missing in signature", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithm); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Signature algorithm parameter is missing in signature", exception.getMessage()); + } + + @Test + void from_sessionCertificateIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Certificate parameter is missing in session status", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Value parameter is missing in certificate", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Certificate level parameter is missing in certificate", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_interactionFlowUsedNotProvided_throwException(String interactionFlowUsed) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionFlowUsed(interactionFlowUsed); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertEquals("Interaction flow used parameter is missing in the session status", exception.getMessage()); + } + + @Test + void from_certificateIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); + var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + + var exception = assertThrows(SmartIdClientException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); + } + + private static SessionResult toSessionResult(String documentNumber) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber(documentNumber); + return sessionResult; + } + + private static SessionSignature toSessionSignature(String sha512WithRSAEncryption) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("serverRandom"); + sessionSignature.setSignatureAlgorithm(sha512WithRSAEncryption); + return sessionSignature; + } + + private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(AUTH_CERT); + sessionCertificate.setCertificateLevel(QUALIFIED); + return sessionCertificate; + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + sessionStatus.setDeviceIpAddress("0.0.0.0"); + return sessionStatus; + } + + private static X509Certificate toX509Certificate(String certificateValue) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateValue.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + private static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java index a5464ca7..b1aa6c7e 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -57,10 +57,11 @@ import org.mockito.ArgumentCaptor; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; class DynamicLinkAuthenticationSessionRequestBuilderTest { @@ -82,7 +83,7 @@ void initAuthenticationSession_ok() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -91,6 +92,7 @@ void initAuthenticationSession_ok() { assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); assertEquals("DEMO", request.getRelyingPartyName()); + assertEquals("QUALIFIED", request.getCertificateLevel()); assertEquals(SignatureProtocol.ACSP_V1, request.getSignatureProtocol()); assertNotNull(request.getSignatureProtocolParameters()); assertNotNull(request.getSignatureProtocolParameters().getRandomChallenge()); @@ -110,7 +112,7 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve .withRelyingPartyName("DEMO") .withCertificateLevel(certificateLevel) .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -131,7 +133,7 @@ void initAuthenticationSession_nonce_ok(String nonce) { .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) .withNonce(nonce) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -152,7 +154,7 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) .withSignatureAlgorithm(signatureAlgorithm) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -171,7 +173,7 @@ void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_o .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -191,7 +193,7 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .withShareMdClientIpAddress(ipRequested) .initAuthenticationSession(); @@ -212,7 +214,7 @@ void initAuthenticationSession_capabilities_ok(String[] capabilities, Set interactions) { + void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -313,22 +315,9 @@ void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(Li assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); } - @ParameterizedTest - @ArgumentsSource(NotSupportedInteractionsProvider.class) - void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(List.of(interaction)) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - @ParameterizedTest @ArgumentsSource(InvalidInteractionsProvider.class) - void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { + public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(DynamicLinkInteraction interaction, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new DynamicLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -390,7 +379,7 @@ private void initAuthentication() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); } } @@ -404,7 +393,7 @@ void initAuthenticationSession_withSemanticsIdentifier() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) .initAuthenticationSession(); @@ -424,7 +413,7 @@ void initAuthenticationSession_withDocumentNumber() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))) + .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .withDocumentNumber("PNOEE-48010010101-MOCK-Q") .initAuthenticationSession(); @@ -500,29 +489,17 @@ public Stream provideArguments(ExtensionContext context) { } } - private static class NotSupportedInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("VERIFICATION_CODE_CHOICE interaction used", Interaction.verificationCodeChoice("Log into internet banking system")), - "AllowedInteractionsOrder contains not supported interaction VERIFICATION_CODE_CHOICE"), - Arguments.of(Named.of("CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE interaction used", Interaction.confirmationMessageAndVerificationCodeChoice("Log into internet banking system")), - "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE") - ); - } - } - private static class InvalidInteractionsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(Named.of("provided text is null", Interaction.displayTextAndPIN(null)), + Arguments.of(Named.of("provided text is null", DynamicLinkInteraction.displayTextAndPIN(null)), "displayText60 cannot be null for AllowedInteractionOrder of type DISPLAY_TEXT_AND_PIN"), - Arguments.of(Named.of("provided text is longer than allowed 60", Interaction.displayTextAndPIN("a".repeat(61))), + Arguments.of(Named.of("provided text is longer than allowed 60", DynamicLinkInteraction.displayTextAndPIN("a".repeat(61))), "displayText60 must not be longer than 60 characters"), - Arguments.of(Named.of("provided text is null", Interaction.confirmationMessage(null)), + Arguments.of(Named.of("provided text is null", DynamicLinkInteraction.confirmationMessage(null)), "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE"), - Arguments.of(Named.of("provided text is longer than allowed 200", Interaction.confirmationMessage("a".repeat(201))), + Arguments.of(Named.of("provided text is longer than allowed 200", DynamicLinkInteraction.confirmationMessage("a".repeat(201))), "displayText200 must not be longer than 200 characters") ); } diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java index 9890c256..21effebf 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -42,6 +42,7 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; class DynamicLinkCertificateChoiceSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java index b1d32187..0b247819 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -56,9 +56,10 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; class DynamicLinkSignatureSessionRequestBuilderTest { @@ -73,7 +74,7 @@ void setUp() { builder = new DynamicLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())) .withCertificateChoiceMade(false); } @@ -318,7 +319,7 @@ void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlg @ParameterizedTest @NullAndEmptySource - void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { + void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { builder.withAllowedInteractionsOrder(allowedInteractionsOrder); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); @@ -365,15 +366,6 @@ void initSignatureSession_whenSignableHashNotFilled() { var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); } - - @ParameterizedTest - @ArgumentsSource(UnsupportedInteractionArgumentsProvider.class) - void initSignatureSession_withNotSupportedInteractionType(Interaction interaction, String expectedErrorMessage) { - builder.withAllowedInteractionsOrder(List.of(interaction)); - - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals(expectedErrorMessage, ex.getMessage()); - } } @Nested @@ -461,16 +453,4 @@ public Stream provideArguments(ExtensionContext context) { return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); } } - - private static class UnsupportedInteractionArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Interaction.verificationCodeChoice("Please verify the code"), - "AllowedInteractionsOrder contains not supported interaction VERIFICATION_CODE_CHOICE"), - Arguments.of(Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm and verify the code"), - "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE") - ); - } - } } diff --git a/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java new file mode 100644 index 00000000..c2f9cf3f --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java @@ -0,0 +1,91 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; + +class ErrorResultHandlerTest { + + @Test + void handle_nullInput() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); + assertEquals("Session end result is not provided", smartIdClientException.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void handle_notOKEndResults(String endResult, Class expectedException) { + assertThrows(expectedException, () -> ErrorResultHandler.handle(endResult)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "UNKNOWN"}) + void handle_unknownEndResult(String unknownEndResult) { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(unknownEndResult)); + assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); + } + + static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java index 3359131b..0e49b53a 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -57,10 +57,11 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; import ee.sk.smartid.v3.rest.dao.VerificationCode; class NotificationAuthenticationSessionRequestBuilderTest { @@ -83,7 +84,7 @@ void initAuthenticationSession_ok() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession(); @@ -109,7 +110,7 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve .withRelyingPartyName("DEMO") .withCertificateLevel(certificateLevel) .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession(); @@ -130,7 +131,7 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) .withSignatureAlgorithm(signatureAlgorithm) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession(); @@ -151,7 +152,7 @@ void initAuthenticationSession_withNonce() { .withRandomChallenge(generateBase64String("a".repeat(32))) .withNonce("uniqueNonce") .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); @@ -171,7 +172,7 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withShareMdClientIpAddress(ipRequested) .initAuthenticationSession(); @@ -192,7 +193,7 @@ void initAuthenticationSession_capabilities_ok(String[] capabilities, Set interactions) { + void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { var exception = assertThrows(SmartIdClientException.class, () -> new NotificationAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -313,23 +314,9 @@ void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(Li assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); } - @ParameterizedTest - @ArgumentsSource(NotSupportedInteractionsProvider.class) - void initAuthenticationSession_allowedInteractionsOrderContainsNotSupportedInteraction_throwException(Interaction interaction, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(interaction)) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - @ParameterizedTest @ArgumentsSource(InvalidInteractionsProvider.class) - void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(Interaction interaction, String expectedException) { + void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(NotificationInteraction interaction, String expectedException) { var exception = assertThrows(SmartIdClientException.class, () -> new NotificationAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -348,7 +335,7 @@ void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwExcept .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession()); assertEquals("Either documentNumber or semanticsIdentifier must be set.", exception.getMessage()); @@ -369,7 +356,7 @@ void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession(); }); @@ -388,7 +375,7 @@ void initAuthenticationSession_vcIsNotPresentInTheResponse_throwException(Verifi .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession(); }); @@ -406,7 +393,7 @@ void initAuthenticationSession_missingVcType_throwException() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession()); @@ -424,7 +411,7 @@ void initAuthenticationSession_unsupportedVcType_throwException() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession()); @@ -442,7 +429,7 @@ void initAuthenticationSession_missingVcValue_throwException() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .initAuthenticationSession()); @@ -512,27 +499,17 @@ public Stream provideArguments(ExtensionContext context) { } } - private static class NotSupportedInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Interaction.displayTextAndPIN("PIN code display"), "AllowedInteractionsOrder contains not supported interaction DISPLAY_TEXT_AND_PIN"), - Arguments.of(Interaction.confirmationMessage("Confirmation message"), "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE") - ); - } - } - private static class InvalidInteractionsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(Named.of("provided text is null", Interaction.verificationCodeChoice(null)), + Arguments.of(Named.of("provided text is null", NotificationInteraction.verificationCodeChoice(null)), "displayText60 cannot be null for AllowedInteractionOrder of type VERIFICATION_CODE_CHOICE"), - Arguments.of(Named.of("provided text is longer than allowed 60", Interaction.verificationCodeChoice("a".repeat(61))), + Arguments.of(Named.of("provided text is longer than allowed 60", NotificationInteraction.verificationCodeChoice("a".repeat(61))), "displayText60 must not be longer than 60 characters"), - Arguments.of(Named.of("provided text is null", Interaction.confirmationMessageAndVerificationCodeChoice(null)), + Arguments.of(Named.of("provided text is null", NotificationInteraction.confirmationMessageAndVerificationCodeChoice(null)), "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE"), - Arguments.of(Named.of("provided text is longer than allowed 200", Interaction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))), + Arguments.of(Named.of("provided text is longer than allowed 200", NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))), "displayText200 must not be longer than 200 characters") ); } diff --git a/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java index b2eaff65..34bc7fba 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -54,9 +54,10 @@ import org.mockito.ArgumentCaptor; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; class NotificationCertificateChoiceSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java index 89959521..cdf9989f 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java @@ -57,9 +57,10 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v3.rest.dao.VerificationCode; @@ -75,7 +76,7 @@ void setUp() { builder = new NotificationSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableData(new SignableData("Test data".getBytes())); } @@ -223,7 +224,6 @@ void initSignatureSession_withCapabilities(Set capabilities, Set when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -324,7 +324,7 @@ void initSignatureSession_whenHashTypeIsNull() { @ParameterizedTest @NullAndEmptySource - void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { + void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { builder.withAllowedInteractionsOrder(allowedInteractionsOrder); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); @@ -363,15 +363,6 @@ void initSignatureSession_emptyNonce() { assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); } - @ParameterizedTest - @ArgumentsSource(InvalidInteractionProvider.class) - void validateAllowedInteractions_containsUnsupportedInteraction(Interaction interaction, String expectedMessage) { - builder.withAllowedInteractionsOrder(List.of(interaction)); - - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals(expectedMessage, ex.getMessage()); - } - @Test void initSignatureSession_whenSignableHashNotFilled() { var signableHash = new SignableHash(); @@ -520,16 +511,6 @@ public Stream provideArguments(ExtensionContext context) { } } - private static class InvalidInteractionProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Interaction.displayTextAndPIN("Display text and PIN"), "AllowedInteractionsOrder contains not supported interaction DISPLAY_TEXT_AND_PIN"), - Arguments.of(Interaction.confirmationMessage("Confirmation message"), "AllowedInteractionsOrder contains not supported interaction CONFIRMATION_MESSAGE") - ); - } - } - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java new file mode 100644 index 00000000..00ba57bd --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java @@ -0,0 +1,251 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionSignature; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +class SignatureResponseMapperTest { + + // TODO - 10.12.24: replace this with signing certificate + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + @Test + void from_sessionResultNull() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setResult(null); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("Result is missing in the session status response", ex.getMessage()); + } + + @Test + void from_missingInteractionFlowUsed() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setInteractionFlowUsed(null); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); + } + + @Test + void from_missingDocumentNumber() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getResult().setDocumentNumber(null); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("Document number is missing in the session result", ex.getMessage()); + } + + @Nested + class CertificateValidation { + + @Test + void from_missingCertificate() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setCert(null); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("Missing certificate in session response", ex.getMessage()); + } + + @Test + void from_missingCertificateValue() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getCert().setValue(null); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("Missing certificate in session response", ex.getMessage()); + } + + @Test + void from_certificateLevelMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getCert().setCertificateLevel("ADVANCED"); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); + } + + @Test + void from_withQscdRequestedAndQualifiedReturned() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getCert().setCertificateLevel("QUALIFIED"); + + SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QSCD", "expectedDigest"); + assertEquals("QUALIFIED", response.getCertificateLevel()); + } + } + + @Nested + class SignatureValidation { + + @Test + void from_validRawDigestSignature() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest"); + assertEquals("OK", response.getEndResult()); + } + + @Test + void from_rawDigestSignatureMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("wrongDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertTrue(ex.getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); + } + + @Test + void from_rawDigestUnexpectedAlgorithm() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); + } + + @Test + void from_unknownSignatureProtocol() { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "UNKNOWN_PROTOCOL", "sha512WithRSAEncryption"); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + + assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_handleSessionEndResultErrors(String endResult, Class expectedException) { + SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getResult().setEndResult(endResult); + + assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + } + + @Test + void from_sessionStatusNull() { + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(null, "QUALIFIED", "expectedDigest")); + + assertEquals("Session status is null", ex.getMessage()); + } + } + + private static SessionStatus createMockSessionStatus(String signatureValue, String signatureProtocol, String signatureAlgorithm) { + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(getEncodedCertificateData(AUTH_CERT)); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setServerRandom("serverRandomValue"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(signatureProtocol); + sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } + + private static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), + Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) + ); + } + } +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 9200d712..3537ec64 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; @@ -46,8 +47,13 @@ import ee.sk.smartid.FileUtil; import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdRestServiceStubs; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.v3.rest.dao.SessionStatus; class SmartIdClientTest { @@ -74,12 +80,12 @@ void createDynamicLinkCertificateChoice() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -96,8 +102,6 @@ void createNotificationCertificateChoice_withSemanticsIdentifier() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) @@ -111,8 +115,6 @@ void createNotificationCertificateChoice_withDocumentNumber() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .withDocumentNumber("PNOEE-1234567890-MOCK-Q") @@ -126,50 +128,44 @@ void createNotificationCertificateChoice_withDocumentNumber() { @WireMockTest(httpPort = 18089) class DynamicLinkAuthenticationSession { - @Test - void createDynamicLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } - @Test - void createDynamicLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); } - @Test - void createDynamicLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) - .initAuthenticationSession(); + @Test + void createDynamicLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + .initAuthenticationSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -189,14 +185,12 @@ void createDynamicLinkSignature_withDocumentNumber() { signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -211,14 +205,12 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); @@ -237,7 +229,7 @@ void createNotificationAuthentication_withSemanticsIdentifier() { NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession(); assertNotNull(response.getSessionID()); @@ -253,7 +245,7 @@ void createNotificationAuthentication_withDocumentNumber() { NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession(); assertNotNull(response.getSessionID()); @@ -275,11 +267,9 @@ void createNotificationSignature_withDocumentNumber() { signableHash.setHashType(HashType.SHA512); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -302,7 +292,7 @@ void createNotificationSignature_withSemanticsIdentifier() { .withRelyingPartyName("DEMO") .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -321,11 +311,21 @@ class SessionsStatus { void fetchFinalSessionStatus() { SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); - SessionStatus status = smartIdClient.createSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); + SessionStatus status = smartIdClient.getSessionsStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); assertEquals("COMPLETE", status.getState()); assertEquals("OK", status.getResult().getEndResult()); } + + @Test + void getSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-running.json"); + + SessionStatus status = smartIdClient.getSessionsStatusPoller().getSessionsStatus("abcdef1234567890"); + + assertEquals("RUNNING", status.getState()); + assertNull(status.getResult()); + } } @Nested @@ -337,15 +337,13 @@ class DynamicContent { void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))) + .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, response.getSessionSecret(), 1); + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, 1, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() .withDynamicLinkType(dynamicLinkType) .withSessionType(SessionType.AUTHENTICATION) @@ -358,7 +356,6 @@ void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLin assertUri(qrCodeUri, SessionType.AUTHENTICATION, dynamicLinkType, response.getSessionToken()); } - @ParameterizedTest @EnumSource void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { @@ -366,14 +363,12 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, 1, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() .withDynamicLinkType(dynamicLinkType) .withSessionType(SessionType.CERTIFICATE_CHOICE) @@ -392,14 +387,12 @@ void createDynamicContent_createQrCode() { SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, response.getSessionSecret(), 1); + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, 1, response.getSessionSecret()); String qrCodeDataUri = smartIdClient.createDynamicContent() .withDynamicLinkType(DynamicLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java deleted file mode 100644 index b89a59af..00000000 --- a/src/test/java/ee/sk/smartid/v3/SmartIdRequestBuilderServiceTest.java +++ /dev/null @@ -1,394 +0,0 @@ -package ee.sk.smartid.v3; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.security.KeyStore; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v3.rest.SessionStatusPoller; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SignatureAlgorithmParameters; - -class SmartIdRequestBuilderServiceTest { - - private SmartIdRequestBuilderService service; - - private static String DEMO_HOST_SSL_CERTIFICATE; - - @BeforeAll - static void loadCertificate() throws IOException { - try (InputStream is = SmartIdRequestBuilderServiceTest.class.getResourceAsStream("/sid_demo_sk_ee.pem"); - BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - - String certContent = reader.lines().collect(Collectors.joining("\n")); - - DEMO_HOST_SSL_CERTIFICATE = certContent - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replaceAll("\\s+", ""); - } - } - - @BeforeEach - public void setUp() throws Exception { - service = new SmartIdRequestBuilderService(); - var client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - - InputStream is = getClass().getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - client.setTrustStore(trustStore); - } - - @Test - void documentFetchingSessionStatus() { - SmartIdConnector connector = mock(SmartIdConnector.class); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - - var mockSessionStatus = new SessionStatus(); - mockSessionStatus.setState("COMPLETE"); - mockSessionStatus.setResult(sessionResult); - - when(connector.getSessionStatus(anyString())).thenReturn(mockSessionStatus); - - var poller = new SessionStatusPoller(connector); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus("mocked_session_id"); - - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - } - - @Test - void createSmartIdAuthenticationResponse_validSessionStatus() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); - assertEquals("QUALIFIED", response.getCertificateLevel()); - assertEquals("OK", response.getEndResult()); - } - - @Test - void createSmartIdAuthenticationResponse_sessionResultNull() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setResult(null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Result is missing in the session status response", ex.getMessage()); - } - - @Test - void createSmartIdAuthenticationResponse_missingInteractionFlowUsed() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.setInteractionFlowUsed(null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); - } - - @Test - void createSmartIdAuthenticationResponse_missingDocumentNumber() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.getResult().setDocumentNumber(null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Document number is missing in the session result", ex.getMessage()); - } - - @Nested - class CertificateValidation { - - @Test - void createSmartIdAuthenticationResponse_missingCertificate() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.setCert(null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Missing certificate in session response", ex.getMessage()); - } - - @Test - void createSmartIdAuthenticationResponse_missingCertificateValue() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.getCert().setValue(null); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Missing certificate in session response", ex.getMessage()); - } - - @Test - void createSmartIdAuthenticationResponse_certificateLevelMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.getCert().setCertificateLevel("ADVANCED"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); - } - - @Test - void createSmartIdAuthenticationResponse_withQscdRequestedAndQualifiedReturned() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.getCert().setCertificateLevel("QUALIFIED"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - service.certificateLevel = "QSCD"; - - SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QSCD", "expectedDigest", "randomChallenge"); - assertEquals("QUALIFIED", response.getCertificateLevel()); - } - } - - @Nested - class SignatureValidation { - - @Test - void createSmartIdAuthenticationResponse_validRawDigestSignature() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse response = service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); - assertEquals("OK", response.getEndResult()); - } - - @Test - void createSmartIdAuthenticationResponse_rawDigestSignatureMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("wrongDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertTrue(ex.getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); - } - - @Test - void createSmartIdAuthenticationResponse_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm", null); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); - } - - @Test - void createSmartIdAuthenticationResponse_acspV1SignatureMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("wrongSignatureValue", "ACSP_V1", - "sha512WithRSAEncryption", "SHA-512"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - SmartIdClientException ex = assertThrows(SmartIdClientException.class, () -> - service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, "randomChallenge")); - - assertTrue(ex.getMessage().contains("ACSP_V1 signature validation failed")); - } - - @Test - void createSmartIdAuthenticationResponse_acspV1NoSuchAlgorithmException() { - SessionStatus sessionStatus = createMockSessionStatus(null, "ACSP_V1", - "sha512WithRSAEncryption", "INVALID_ALGORITHM"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - SmartIdClientException ex = assertThrows(SmartIdClientException.class, () -> - service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", null, "randomChallenge")); - - assertEquals("Error while creating digest for ACSP_V1 signature validation", ex.getMessage()); - } - - @Test - void createSmartIdAuthenticationResponse_unknownSignatureProtocol() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "UNKNOWN_PROTOCOL", "sha512WithRSAEncryption", null); - sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(SmartIdClientException.class, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void createSmartIdAuthenticationResponse_handleSessionEndResultErrors(String endResult, Class expectedException) { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption", null); - sessionStatus.getResult().setEndResult(endResult); - - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - assertThrows(expectedException, () -> service.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge")); - } - - @Test - void createSmartIdAuthenticationResponse_sessionStatusNull() { - service.dataToSign = new SignableData("dataToBeSigned".getBytes(StandardCharsets.UTF_8)); - service.dataToSign.setHashType(HashType.SHA512); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> service.createSmartIdAuthenticationResponse(null, "QUALIFIED", "expectedDigest", "randomChallenge")); - - assertEquals("Session status is null", ex.getMessage()); - } - } - - private static SessionStatus createMockSessionStatus(String signatureValue, String signatureProtocol, String signatureAlgorithm, String hashAlgorithm) { - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(DEMO_HOST_SSL_CERTIFICATE); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setServerRandom("serverRandomValue"); - - if ("ACSP_V1".equals(signatureProtocol)) { - var sigAlgParams = new SignatureAlgorithmParameters(); - sigAlgParams.setHashAlgorithm(hashAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(sigAlgParams); - } - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(signatureProtocol); - sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); - - return sessionStatus; - } - - static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("USER_REFUSED", UserRefusedException.class), - Arguments.of("TIMEOUT", SessionTimeoutException.class), - Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), - Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), - Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), - Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), - Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) - ); - } - } -} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java new file mode 100644 index 00000000..1b65683b --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java @@ -0,0 +1,81 @@ +package ee.sk.smartid.v3.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +class SessionStatusPollerTest { + + private SmartIdConnector smartIdConnector; + + private SessionStatusPoller poller; + + @BeforeEach + void setUp() { + smartIdConnector = mock(SmartIdConnector.class); + poller = new SessionStatusPoller(smartIdConnector); + } + + @Test + void fetchFinalSessionStatus() { + SessionStatus runningStatus = new SessionStatus(); + runningStatus.setState("RUNNING"); + + SessionStatus completedStatus = new SessionStatus(); + completedStatus.setState("COMPLETE"); + + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")) + .thenReturn(runningStatus, completedStatus); + + SessionStatus finalSessionStatus = poller.fetchFinalSessionStatus("00000000-0000-0000-0000-000000000000"); + + verify(smartIdConnector, times(2)).getSessionStatus("00000000-0000-0000-0000-000000000000"); + assertEquals("COMPLETE", finalSessionStatus.getState()); + } + + @Test + void getSessionsStatus() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("RUNNING"); + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); + + SessionStatus sessionsStatus = poller.getSessionsStatus("00000000-0000-0000-0000-000000000000"); + + assertEquals("RUNNING", sessionsStatus.getState()); + assertNull(sessionsStatus.getResult()); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index 73fdcf78..a9cbbe29 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -58,18 +58,19 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; class SmartIdRestConnectorTest { @@ -917,13 +918,14 @@ private AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); + dynamicLinkAuthenticationSessionRequest.setCertificateLevel("QUALIFIED"); var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); - Interaction interaction = Interaction.displayTextAndPIN("Log in?"); + DynamicLinkInteraction interaction = DynamicLinkInteraction.displayTextAndPIN("Log in?"); dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); return dynamicLinkAuthenticationSessionRequest; @@ -939,7 +941,7 @@ private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest( signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); - Interaction interaction = Interaction.verificationCodeChoice("Verify the code"); + NotificationInteraction interaction = NotificationInteraction.verificationCodeChoice("Verify the code"); dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); return dynamicLinkAuthenticationSessionRequest; @@ -965,7 +967,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { request.setSignatureProtocolParameters(protocolParameters); - request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Sign the document"))); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign the document"))); return request; } @@ -980,7 +982,7 @@ private SignatureSessionRequest createNotificationSignatureSessionRequest() { protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); request.setSignatureProtocolParameters(protocolParameters); - request.setAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Verify the code"))); + request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))); return request; } diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java index ade82c88..9587b8dc 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,13 +33,13 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.v3.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v3.RandomChallenge; import ee.sk.smartid.v3.SignatureAlgorithm; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; @Disabled("Currently request to v3 path returns - No permission to issue the request") @SmartIdDemoIntegrationTest @@ -56,7 +56,7 @@ void setUp() { void authenticate_anonymous() { AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); DynamicLinkSessionResponse response = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); } @@ -65,7 +65,7 @@ void authenticate_anonymous() { void authenticate_withDocumentNumber() { AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-50609019996-MOCK-Q"); } @@ -74,7 +74,7 @@ void authenticate_withDocumentNumber() { void authenticate_withSemanticsIdentifier() { AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Log in?"))); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); } diff --git a/src/test/resources/test-certs/auth-cert-40504040001.pem.crt b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt new file mode 100644 index 00000000..cfaeac6b --- /dev/null +++ b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQy +bEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7C +XahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEO +TnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5n +zCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcy +hPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY +0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29Jc +owTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943 +iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9Ps +mN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1Jg +xBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr +/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRj +Fisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QA +CBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq +8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v0 +9hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSg +At+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHx +MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYI +KwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJ +RC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5z +ay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQw +NTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsG +AQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9j +ZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1Ud +CQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0G +CysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rl +c3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHK +mTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0X +nPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxW +Cbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2 +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/ca-cert.pem.crt b/src/test/resources/test-certs/ca-cert.pem.crt new file mode 100644 index 00000000..5ca038c0 --- /dev/null +++ b/src/test/resources/test-certs/ca-cert.pem.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/expired-cert.pem.crt b/src/test/resources/test-certs/expired-cert.pem.crt new file mode 100644 index 00000000..8e18defd --- /dev/null +++ b/src/test/resources/test-certs/expired-cert.pem.crt @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkG +A1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYD +VQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0G +CSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51ISh +Sj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+Ml +lSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5cz +GP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdc +DmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncv +pRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhN +BYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04 +nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a +4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2 +wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSno +JPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoA +xoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIE +sDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6 +Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4E +FgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtm +Vf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkG +CCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEF +BQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tf +MjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdF +TOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDi +G7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbr +rl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxM +TEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl +4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1 +ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y5 +2n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf +83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAK +Gn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbR +R1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2P +MnXRUgtlnV7ykFpmkR4JcQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/other-auth-cert.pem.crt b/src/test/resources/test-certs/other-auth-cert.pem.crt new file mode 100644 index 00000000..5ad5e65e --- /dev/null +++ b/src/test/resources/test-certs/other-auth-cert.pem.crt @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcw +FQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTAL +BgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEw +DQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnU +hKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz +4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6z +lzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8 +l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpe +dy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0U +aE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0w +LTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2n +T5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339z +t7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKx +Kegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XK +ygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD +AgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRw +czovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1Ud +DgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XM +C2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4w +KQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsG +AQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNL +XzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifm +rjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2p +Kmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6v +ZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7 +grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4 +lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3Fa +YpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8D +j/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5o +PEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5 +m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql4 +40sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytf +q8s5bZci5vnHm110lnPhQxM= +-----END CERTIFICATE----- diff --git a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json index bb38ec31..cb28691c 100644 --- a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json +++ b/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json @@ -6,7 +6,11 @@ "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", "signatureAlgorithm": "sha512WithRSAEncryption" }, + "certificateLevel": "QUALIFIED", "allowedInteractionsOrder": [ - {"type": "displayTextAndPIN", "displayText60": "Log in?"} + { + "type": "displayTextAndPIN", + "displayText60": "Log in?" + } ] } \ No newline at end of file diff --git a/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json b/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json index 24832a88..b682a0e7 100644 --- a/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json +++ b/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json @@ -1,5 +1,5 @@ { "sessionID": "00000000-0000-0000-0000-000000000000", "sessionToken": "sessionToken", - "sessionSecret": "sessionSecret" + "sessionSecret": "c2Vzc2lvblNlY3JldA==" } \ No newline at end of file diff --git a/src/test/resources/v3/responses/session-status-running.json b/src/test/resources/v3/responses/session-status-running.json new file mode 100644 index 00000000..221b89f7 --- /dev/null +++ b/src/test/resources/v3/responses/session-status-running.json @@ -0,0 +1,4 @@ +{ + "state": "RUNNING", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file From 4843d51cecccb648f89b537eef07af78c887953a Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 15 Jan 2025 08:52:24 +0200 Subject: [PATCH 14/57] SLIB-75 - added missing parameters validations (#98) * SLIB-75 - added missing parameters validations * SLIB-75 - refactored tests, removed unnecessary validation * SLIB-75 - fixed error handling * SLIB-75 - removed SignatureAlgorithmParameters usage --------- Co-authored-by: ragnar.haide --- README.md | 6 +- .../ee/sk/smartid/v3/CertificateLevel.java | 20 ++- .../smartid/v3/SignatureResponseMapper.java | 89 +++++++---- .../smartid/v3/rest/dao/SessionSignature.java | 8 - .../dao/SignatureAlgorithmParameters.java | 44 ------ .../v3/SignatureResponseMapperTest.java | 149 +++++++++++++----- 6 files changed, 183 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java diff --git a/README.md b/README.md index 1f37cbc3..0634b281 100644 --- a/README.md +++ b/README.md @@ -810,6 +810,7 @@ The session status response includes various fields depending on whether the ses * `state`: RUNNING or COMPLETE * `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) +* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. * `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature sessions) * `signature`: Contains the following fields based on the signatureProtocol used: * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm @@ -1113,8 +1114,6 @@ The request parameters for the dynamic link signature session are as follows: * `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. * `digest`: Required. Base64 encoded digest to be signed. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. - * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. * `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. * Each interactionDeprecated object includes: * `type`: Required. Type of interactionDeprecated. Allowed types are `displayTextAndPIN`, `confirmationMessage`. @@ -1292,7 +1291,6 @@ builder.withAllowedInteractionsOrder(List.of( var parameters = new RawDigestSignatureProtocolParameters(); parameters.setDigest(signableData.calculateHashInBase64()); parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); -parameters.setSignatureAlgorithmParameters(new SignatureAlgorithmParameters("SHA-512")); builder.withSignatureProtocolParameters(parameters); ``` @@ -1345,8 +1343,6 @@ The request parameters for the notification-based signature session are as follo * `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. * `digest`: Required. Base64 encoded digest to be signed. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. - * `signatureAlgorithmParameters`: Optional. Additional parameters if required by the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`. * `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. * Each interactionDeprecated object includes: * `type`: Required. Type of interactionDeprecated. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. diff --git a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java index 5b35a662..8de6222f 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java @@ -27,5 +27,23 @@ */ public enum CertificateLevel { - ADVANCED, QUALIFIED, QSCD + ADVANCED(1), + QUALIFIED(2), + QSCD(2); + + private final int level; + + CertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return the level of the certificate + */ + public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { + return this == certificateLevel || this.level > certificateLevel.level; + } } diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java index fd49755c..10574b9d 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java @@ -56,7 +56,6 @@ public class SignatureResponseMapper { * * @param sessionStatus session status response * @param requestedCertificateLevel certificate level used to start the signature session - * @param digest data that was sent for signing, will be used to validate signature * @return the signature response * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame @@ -64,11 +63,10 @@ public class SignatureResponseMapper { * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. */ public static SingatureResponse from(SessionStatus sessionStatus, - String requestedCertificateLevel, - String digest + String requestedCertificateLevel ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - validateSessionsStatus(sessionStatus, requestedCertificateLevel, digest); + validateSessionsStatus(sessionStatus, requestedCertificateLevel); SessionResult sessionResult = sessionStatus.getResult(); SessionSignature sessionSignature = sessionStatus.getSignature(); @@ -88,45 +86,66 @@ public static SingatureResponse from(SessionStatus sessionStatus, return singatureResponse; } - private static void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest) { + private static void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { if (sessionStatus == null) { - throw new UnprocessableSmartIdResponseException("Session status is null"); + throw new SmartIdClientException("Session status was not provided"); } - validateSessionResult(sessionStatus, requestedCertificateLevel, expectedDigest); + + if (StringUtil.isEmpty(sessionStatus.getState())) { + throw new UnprocessableSmartIdResponseException("State parameter is missing in session status"); + } + + if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); + } + + validateSessionResult(sessionStatus, requestedCertificateLevel); } - private static void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel, String expectedDigest) { + private static void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel) { SessionResult sessionResult = sessionStatus.getResult(); if (sessionResult == null) { logger.error("Result is missing in the session status response"); - throw new SmartIdClientException("Result is missing in the session status response"); + throw new UnprocessableSmartIdResponseException("Result is missing in the session status response"); } String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); + } + if ("OK".equalsIgnoreCase(endResult)) { logger.info("Session completed successfully"); if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { logger.error("Document number is missing in the session result"); - throw new SmartIdClientException("Document number is missing in the session result"); + throw new UnprocessableSmartIdResponseException("Document number is missing in the session result"); } if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { logger.error("InteractionFlowUsed is missing in the session status"); - throw new SmartIdClientException("InteractionFlowUsed is missing in the session status"); + throw new UnprocessableSmartIdResponseException("InteractionFlowUsed is missing in the session status"); + } + + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); } validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - validateSignature(sessionStatus, expectedDigest); + validateSignature(sessionStatus); } else { ErrorResultHandler.handle(endResult); } } private static void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { - if (sessionCertificate == null || sessionCertificate.getValue() == null) { - throw new SmartIdClientException("Missing certificate in session response"); + if (sessionCertificate == null || StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Missing certificate in session response"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate level is missing in certificate"); } try { @@ -143,37 +162,43 @@ private static void validateCertificate(SessionCertificate sessionCertificate, S } private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { - CertificateLevel requestedLevelEnum = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); - CertificateLevel returnedLevelEnum = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); + CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); + CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); - // TODO - 03.12.24: does not validate if returned certificate level is same or higher as requested - return requestedLevelEnum == CertificateLevel.QSCD ? returnedLevelEnum == CertificateLevel.QUALIFIED : requestedLevelEnum == returnedLevelEnum; + return returnedLevel.isSameLevelOrHigher(requestedLevel); } - private static void validateSignature(SessionStatus sessionStatus, String expectedDigest) { + private static void validateSignature(SessionStatus sessionStatus) { String signatureProtocol = sessionStatus.getSignatureProtocol(); if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { - validateRawDigestSignature(sessionStatus, expectedDigest); + validateRawDigestSignature(sessionStatus); } else { - throw new SmartIdClientException("Unknown signature protocol: " + signatureProtocol); + throw new UnprocessableSmartIdResponseException("Unknown signature protocol: " + signatureProtocol); } } - private static void validateRawDigestSignature(SessionStatus sessionStatus, String expectedDigest) { - String signatureValue = sessionStatus.getSignature().getValue(); - String signatureAlgorithm = sessionStatus.getSignature().getSignatureAlgorithm(); + private static void validateRawDigestSignature(SessionStatus sessionStatus) { + SessionSignature signature = sessionStatus.getSignature(); + if (signature == null) { + throw new UnprocessableSmartIdResponseException("Signature object is missing"); + } + + if (StringUtil.isEmpty(signature.getValue())) { + throw new UnprocessableSmartIdResponseException("Signature value is missing"); + } - if (!expectedDigest.equals(signatureValue)) { // TODO - 10.12.24: fix this, validating signature should be like in AuthenticationResponseMapper.validateSignature - throw new SmartIdClientException("RAW_DIGEST_SIGNATURE validation failed. Expected: " + expectedDigest - + ", but got: " + signatureValue); + if (StringUtil.isEmpty(signature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature algorithm is missing"); } - List allowedSignatureAlgorithms = Arrays.asList("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"); - if (!allowedSignatureAlgorithms.contains(signatureAlgorithm)) { - throw new SmartIdClientException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signatureAlgorithm); + List allowedSignatureAlgorithms = Arrays.stream(SignatureAlgorithm.values()) + .map(SignatureAlgorithm::getAlgorithmName) + .toList(); + if (!allowedSignatureAlgorithms.contains(signature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signature.getSignatureAlgorithm()); } - logger.info("RAW_DIGEST_SIGNATURE successfully validated."); + logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); } -} +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java index 174e2fe2..3cca24da 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java @@ -36,7 +36,6 @@ public class SessionSignature implements Serializable { private String value; private String serverRandom; private String signatureAlgorithm; - private SignatureAlgorithmParameters signatureAlgorithmParameters; public String getValue() { return value; @@ -62,11 +61,4 @@ public void setSignatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } - public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { - return signatureAlgorithmParameters; - } - - public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { - this.signatureAlgorithmParameters = signatureAlgorithmParameters; - } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java deleted file mode 100644 index 83208030..00000000 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureAlgorithmParameters.java +++ /dev/null @@ -1,44 +0,0 @@ -package ee.sk.smartid.v3.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SignatureAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - - public String getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } -} diff --git a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java index 00ba57bd..0c37f740 100644 --- a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java @@ -64,76 +64,122 @@ class SignatureResponseMapperTest { // TODO - 10.12.24: replace this with signing certificate private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + @Test + void from_stateParameterMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setState(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("State parameter is missing in session status", ex.getMessage()); + } + + @Test + void from_sessionNotComplete() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setState("RUNNING"); + + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertTrue(ex.getMessage().contains("Session is not complete")); + } + @Test void from_sessionResultNull() { SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); sessionStatus.setResult(null); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertEquals("Result is missing in the session status response", ex.getMessage()); } @Test - void from_missingInteractionFlowUsed() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setInteractionFlowUsed(null); - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + void from_endResultParameterMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getResult().setEndResult(null); - assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("End result parameter is missing in the session result", ex.getMessage()); } @Test void from_missingDocumentNumber() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getResult().setDocumentNumber(null); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertEquals("Document number is missing in the session result", ex.getMessage()); } + @Test + void from_missingInteractionFlowUsed() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setInteractionFlowUsed(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + + assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); + } + + @Test + void from_signatureProtocolMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setSignatureProtocol(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature protocol is missing in session status", ex.getMessage()); + } + @Nested class CertificateValidation { @Test void from_missingCertificate() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.setCert(null); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertEquals("Missing certificate in session response", ex.getMessage()); } @Test void from_missingCertificateValue() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getCert().setValue(null); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertEquals("Missing certificate in session response", ex.getMessage()); } + @Test + void from_certificateLevelMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getCert().setCertificateLevel(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Certificate level is missing in certificate", ex.getMessage()); + } + @Test void from_certificateLevelMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getCert().setCertificateLevel("ADVANCED"); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); } @Test void from_withQscdRequestedAndQualifiedReturned() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getCert().setCertificateLevel("QUALIFIED"); - SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QSCD", "expectedDigest"); - assertEquals("QUALIFIED", response.getCertificateLevel()); + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QSCD")); + assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); } } @@ -142,40 +188,30 @@ class SignatureValidation { @Test void from_validRawDigestSignature() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest"); + SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); assertEquals("OK", response.getEndResult()); } - @Test - void from_rawDigestSignatureMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("wrongDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); - - assertTrue(ex.getMessage().contains("RAW_DIGEST_SIGNATURE validation failed")); - } - @Test void from_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); } @Test void from_unknownSignatureProtocol() { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "UNKNOWN_PROTOCOL", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("UNKNOWN_PROTOCOL", "sha512WithRSAEncryption"); sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); } @@ -183,22 +219,49 @@ void from_unknownSignatureProtocol() { @ParameterizedTest @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) void from_handleSessionEndResultErrors(String endResult, Class expectedException) { - SessionStatus sessionStatus = createMockSessionStatus("expectedDigest", "RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getResult().setEndResult(endResult); - assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED", "expectedDigest")); + assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); } @Test void from_sessionStatusNull() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(null, "QUALIFIED", "expectedDigest")); + var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(null, "QUALIFIED")); + + assertEquals("Session status was not provided", ex.getMessage()); + } + + @Test + void from_signatureMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setSignature(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature object is missing", ex.getMessage()); + } + + @Test + void from_signatureValueMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getSignature().setValue(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature value is missing", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.getSignature().setSignatureAlgorithm(null); - assertEquals("Session status is null", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature algorithm is missing", ex.getMessage()); } } - private static SessionStatus createMockSessionStatus(String signatureValue, String signatureProtocol, String signatureAlgorithm) { + private static SessionStatus createMockSessionStatus(String signatureProtocol, String signatureAlgorithm) { var sessionResult = new SessionResult(); sessionResult.setEndResult("OK"); @@ -206,10 +269,10 @@ private static SessionStatus createMockSessionStatus(String signatureValue, Stri var sessionCertificate = new SessionCertificate(); sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(getEncodedCertificateData(AUTH_CERT)); + sessionCertificate.setValue(getEncodedCertificateData()); var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); + sessionSignature.setValue("expectedDigest"); sessionSignature.setSignatureAlgorithm(signatureAlgorithm); sessionSignature.setServerRandom("serverRandomValue"); @@ -224,8 +287,8 @@ private static SessionStatus createMockSessionStatus(String signatureValue, Stri return sessionStatus; } - private static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") + private static String getEncodedCertificateData() { + return SignatureResponseMapperTest.AUTH_CERT.replace("-----BEGIN CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "") .replace("\n", ""); } From 5f2b8f7ca5e4a0f9ca1734ab4da2ba1a78645bf9 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:08:38 +0200 Subject: [PATCH 15/57] Update readme to include instructions for rpapi v3 (#100) * SLIB-75 - added missing parameters validations * SLIB-75 - refactored tests, removed unnecessary validation * SLIB-56 - update dynamic-link README description * SLIB-56 - code review fixes * SLIB-56 - code review changes * SLIB-56 - replaced QR-code generation part --------- Co-authored-by: ragnar.haide --- README.md | 1177 +++++++++-------- .../smartid/v3/SignatureResponseMapper.java | 4 + 2 files changed, 609 insertions(+), 572 deletions(-) diff --git a/README.md b/README.md index 0634b281..a2799008 100644 --- a/README.md +++ b/README.md @@ -46,25 +46,51 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Configuring a proxy using JBoss Resteasy library](#configuring-a-proxy-using-jboss-resteasy-library) * [Configuring a proxy using Jersey](#configuring-a-proxy-using-jersey) * [How to use it with RP API v3.0](#how-to-use-api-v30) - * [Initating authentication session](#examples-of-performing-dynamic-link-authentication) - * [Initiating anonymous authentication session](#initiating-anonymous-authentication-session) - * [Initiating authentication session with semantics identifier](#initiating-authentication-session-with-semantics-identifier) - * [Initiating authentication session with document number](#initiating-authentication-session-with-document-number) - * [Querying sessions status](#session-status-request-handling-for-v30) - * [Sessions status response](#session-status-response) - * [Example of fetching session status in v3.0](#example-of-fetching-session-status-in-v30) - * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) - * [Example of querying sessions status](#example-of-querying-sessions-status) - * [Validating sessions status response](#validating-session-status-response) - * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) - * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) - * [Generating dynamic link ](#generating-dynamic-link) + * [Setting up SmartIdClient for v3.0](#setting-up-smartidclient-for-v30) + * [Dynamic Link flows](#dynamic-link-flows) + * [Initiating authentication session](#examples-of-performing-dynamic-link-authentication) + * [Initiating anonymous authentication session](#initiating-anonymous-authentication-session) + * [Initiating authentication session with semantics identifier](#initiating-authentication-session-with-semantics-identifier) + * [Initiating authentication session with document number](#initiating-authentication-session-with-document-number) + * [Initiating a Dynamic Link Certificate Choice Session](#initiating-a-dynamic-link-certificate-choice-session) + * [Example of Initiating a Dynamic Link Certificate Choice Request](#example-initiating-a-dynamic-link-certificate-choice-request) + * [Response on Successful Certificate Choice Session Creation](#response-on-successful-certificate-choice-session-creation) + * [Initiating a Dynamic Link Signature Session](#initiating-a-dynamic-link-signature-session) + * [Initiating a Dynamic Link Signature Session Using Semantics Identifier](#initiating-a-dynamic-link-signature-session-using-semantics-identifier) + * [Initiating a Dynamic Link Signature Session Using Document Number](#initiating-a-dynamic-link-signature-session-using-document-number) + * [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) + * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) + * [Generating dynamic link ](#generating-dynamic-link) * [Dynamic link parameters](#dynamic-link-parameters) * [Overriding default values](#overriding-default-values) - * [Generating QR-code](#generating-qr-code) - * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) - - + * [Generating QR-code](#generating-qr-code) + * [Generate QR-code Data URI](#generate-qr-code-data-uri) + * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) + * [Querying sessions status](#session-status-request-handling-for-v30) + * [Sessions status response](#session-status-response) + * [Example of fetching session status in v3.0](#example-of-fetching-session-status-in-v30) + * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) + * [Example of querying sessions status](#example-of-querying-sessions-status) + * [Validating sessions status response](#validating-session-status-response) + * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) + * [Example of validating the signature](#example-of-validating-the-signature) + * [Error handling for session status](#error-handling-for-session-status) + * [Notification-based flows](#notification-based-flows) + * [Differences Between Notification-Based and Dynamic Link Flows](#differences-between-notification-based-and-dynamic-link-flows) + * [Examples of performing notification authentication](#examples-of-performing-notification-authentication) + * [Initiating notification authentication session with document number](#initiating-notification-authentication-session-with-document-number) + * [Initiating notification authentication session with semantics identifier](#initiating-notification-authentication-session-with-semantics-identifier) + * [Initiating a Notification Certificate Choice Session](#initiating-a-notification-certificate-choice-session) + * [Initiating a Notification Certificate Choice Using Semantics Identifier](#initiating-a-notification-certificate-choice-using-semantics-identifier) + * [Initiating a Notification Certificate Choice Using Document Number](#initiating-a-notification-certificate-choice-using-document-number) + * [Initiating a Notification-Based Signature Session](#initiating-a-notification-based-signature-session) + * [Initiating a Notification-Based Signature Session Using Semantics Identifier](#initiating-a-notification-based-signature-session-using-semantics-identifier) + * [Initiating a Notification-Based Signature Session Using Document Number](#initiating-a-notification-based-signature-session-using-document-number) + * [Response on Successful Notification-based Signature Session Creation](#response-on-successful-notification-based-signature-session-creation) + * [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) + * [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) + * [Exception Handling](#exception-handling) + ## Introduction The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. @@ -698,7 +724,7 @@ String randomChallenge = RandomChallenge.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client +DynamicLinkSessionResponse authenticationSessionResponse = client .createAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRandomChallenge(randomChallenge) @@ -737,7 +763,7 @@ String randomChallenge = RandomChallenge.generate(); // Store generated randomChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client +DynamicLinkSessionResponse authenticationSessionResponse = client .createDynamicLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" @@ -775,7 +801,7 @@ String randomChallenge = RandomChallenge.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DynamicLinkAuthenticationSessionResponse authenticationSessionResponse = client +DynamicLinkSessionResponse authenticationSessionResponse = client .createDynamicLinkAuthentication() .withDocumentNumber(documentNumber) .withRandomChallenge(randomChallenge) @@ -798,166 +824,16 @@ String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse ``` +Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. +Jump to [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. -## Session status request handling for v3.0 - -The Smart-ID v3.0 API includes new session status request paths for retrieving session results. - -## Session status response - -The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: - -* `state`: RUNNING or COMPLETE -* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) -* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. -* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature sessions) -* `signature`: Contains the following fields based on the signatureProtocol used: - * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm - * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm -* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). -* `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionFlowUsed`: The interactionDeprecated flow used for the session. -* `deviceIpAddress`: IP address of the mobile device, if requested. - -## Example of fetching session status in v3.0 - -### Example of using session status poller to query final sessions status -The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. - -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Client setup with TrustStore. Requests will not work without a valid certificate. -InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); -client.setTrustStore(trustStore); - -// -SessionsStatusPoller poller = client.getSessionsStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); - -if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); -} -``` - -### Example of querying sessions status -The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. - -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Client setup with TrustStore. Requests will not work without a valid certificate. -InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); -client.setTrustStore(trustStore); - -// Get the session status poller -SessionsStatusPoller poller = client.getSessionsStatusPoller(); - -// Queryinn -SessionStatus sessionStatus = poller.getSessionsStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); -if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - // Session is still running and querying can be continued - // Dynamic content can be generated and displayed to the user -} else { - // continue to the next step -} -``` - -## Validating session status response - -It's important to validate the session status response to ensure that the returned signature or authentication result is valid. - -* Validate that endResult is OK if the session was successful. -* Check the certificate field to ensure it has the required certificate level and that it is signed by a trusted CA. -* For `ACSP_V1` signature validation, compare the digest of the signature protocol, server random, and random challenge. -* For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. - -### Example of validating the authentication sessions response: - -```java -// init authentication response validator with trusted certificates -// there are 4 different ways to initialize the validator -// 1. use default values `trusted_certificates.jks` with password `changeit` -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); -// 2. provide your own path to truststore and truststore password -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(truststorePath, truststorePassword); -// 3 read trusted certificate yourself and provide it to the validator -X509Certificate[] trustedCertificates = findTrustedCertificates(); -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCertificates); -// 4. init authentication response validator with the empty array and add trusted certificates -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[0]); -X509Certificate certificate = getTrustedCertificate(); -authenticationResponseValidator.addTrustedCACertificate(certificate); - -// get sessions result -SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); - -// validate sessions state is completed -if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - // validate sessions status result and map session status to authentication response - DynamicLinkAuthenticationResponse response = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step - - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.from(response, "randomChallenge"); -} -``` - -### Example of validating the signature: - -```java -SmartIdRequestBuilderService requestBuilder = new SmartIdRequestBuilderService(); -requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); - -SmartIdAuthenticationResponse response = requestBuilder.createSmartIdAuthenticationResponse(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); - -System.out.println("Authentication result: " + response.getEndResult()); -``` - -## Error handling for session status - -The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: - -* `OK`: Session completed successfully. -* `USER_REFUSED`: User refused the session. -* `TIMEOUT`: User did not respond in time. -* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. -* `WRONG_VC`: User selected the wrong verification code. -* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interactionDeprecated is not supported by the user's app. -* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. -* `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). -* `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. -* `USER_REFUSED_CONFIRMATIONMESSAGE`: User cancelled on confirmationMessage screen. -* `USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE`: User cancelled on confirmationMessageAndVerificationCodeChoice screen. - -### The error codes can be validated using the SmartIdRequestBuilderService - -```java -try { - requestBuilder.validateSessionResult(sessionStatus, "QUALIFIED", "expectedDigest", "randomChallenge"); -} catch (UserRefusedException e) { - System.out.println("User refused the session"); -} catch (SessionTimeoutException e) { - System.out.println("Session timed out"); -} -``` - -# Initiating a Dynamic Link Certificate Choice Session in API v3.0 +### Initiating a Dynamic Link Certificate Choice Session +!!!Dynamic-link Certificate Choice session Cannot be used at the moment!!! The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a certificate choice session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the authentication process. -## Request Parameters +#### Request Parameters The request parameters for the dynamic link certificate choice session are: @@ -968,7 +844,7 @@ The request parameters for the dynamic link certificate choice session are: * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. * `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -## Example: Initiating a Dynamic Link Certificate Choice Request +#### Example: Initiating a Dynamic Link Certificate Choice Request Here's an example of how to initiate a dynamic link certificate choice request using the Smart-ID Java client. ```java @@ -988,17 +864,33 @@ DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest // Note: After a certificate choice request, a notification-based signature choice must follow. ``` -## Response on Successful Certificate Choice Session Creation +#### Example of Initiating a dynamic link certificate choice request with `QUALIFIED` certificate level and IP sharing enabled. +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withShareMdClientIpAddress(true) + .initiateCertificateChoice(); +``` + +#### Response on Successful Certificate Choice Session Creation The response from a successful dynamic link certificate choice session creation contains the following parameters: * `sessionID`: A string that can be used to request the operation result. * `sessionToken`: Unique random value that will be used to connect this certificate choice attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -## Validating Parameters +#### Validating Parameters Ensure that you validate the parameters before initiating the request. For example, the `nonce` must be between 1 and 30 characters. -## Error Handling +#### Error Handling Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `SmartIdClientException`, and others. ```java @@ -1012,99 +904,20 @@ try { } ``` -## Additional Information - -### `Request Properties`: If you need the IP address of the user's device, set only shareMdClientIpAddress to true. There is no need to create a full RequestProperties object for this. +#### `Request Properties`: If you need the IP address of the user's device, set only shareMdClientIpAddress to true. There is no need to create a full RequestProperties object for this. ```java client.createDynamicLinkCertificateRequest().withShareMdClientIpAddress(true); ``` -### `Capabilities`: The capabilities parameter is an optional field used only when an agreement is established with the Smart-ID provider. If this parameter is omitted, the requested capabilities are automatically derived from the `certificateLevel`. Supported certificate levels include: +* `Capabilities`: The capabilities parameter is an optional field used only when an agreement is established with the Smart-ID provider. If this parameter is omitted, the requested capabilities are automatically derived from the `certificateLevel`. Supported certificate levels include: * `ADVANCED`: A certificate for advanced electronic signatures. * `QUALIFIED`: A qualified certificate under the eIDAS regulation. * `QSCD`: A qualified certificate that is also QSCD-capable, marking a higher level of security for qualified signatures. -### Example of Initiating a dynamic link certificate choice request with `QUALIFIED` certificate level and IP sharing enabled. -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - - DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withShareMdClientIpAddress(true) - .initiateCertificateChoice(); -``` - -## Initiating a Notification Certificate Choice in API v3.0 - -### Request Parameters -The request parameters for the dynamic link certificate choice session are: - -* `relyingPartyUUID`: UUID of the Relying Party. -* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. - -### Using Semantics Identifier -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "30303039914"); // identifier (according to country and identity type reference) - -NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .withNonce("1234567890") - .initCertificateChoice(); - -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later -``` - -### Using Document Number -```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .withNonce("1234567890") - .initCertificateChoice(); - -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later -``` - -### Requesting the IP Address of the User's Device -If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withShareMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. -```java -NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QUALIFIED) - - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) - .initCertificateChoice(); -``` - -# Initiating a Dynamic Link Signature Session in API v3.0 +### Initiating a Dynamic Link Signature Session The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a signature session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the signing process using a dynamic link. The user can then access the link and complete the signing process. -## Request Parameters +#### Request Parameters The request parameters for the dynamic link signature session are as follows: * `relyingPartyUUID`: Required. UUID of the Relying Party. @@ -1112,74 +925,28 @@ The request parameters for the dynamic link signature session are as follows: * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. * `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. * `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. - * Each interactionDeprecated object includes: - * `type`: Required. Type of interactionDeprecated. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. + * Each interactionDeprecated object includes: + * `type`: Required. Type of interactionDeprecated. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -## Examples of Allowed Interactions Order -An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. Different interactions can support different amounts of data to display information to the user. - -Below are examples of `allowedInteractionsOrder` elements in JSON format: -Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` -Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. +#### Initiating a Dynamic Link Signature Session Using Semantics Identifier ```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessage("Up to 200 characters of text here.."), - Interaction.displayTextAndPIN("Up to 60 characters of text here..") - )); -``` +var client = new SmartIdClient(); + client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + client.setRelyingPartyName("DEMO"); + client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -Example 2: `confirmationMessage` with Fallback to `verificationCodeChoice` -Description: The RP's first choice is `confirmationMessage`; if not available, then use `verificationCodeChoice`. -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessage("Up to 200 characters of text here.."), - Interaction.verificationCodeChoice("Up to 60 characters of text here..") -)); -``` - -Example 3: `displayTextAndPIN` Only -Description: Use `displayTextAndPIN` interactionDeprecated only. -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.displayTextAndPIN("Up to 60 characters of text here..") -)); -``` - -Example 4: `verificationCodeChoice` with Fallback to `displayTextAndPIN` -Description: Use `verificationCodeChoice`; if not available, then `displayTextAndPIN` should be used. -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.verificationCodeChoice("Up to 60 characters of text here.."), - Interaction.displayTextAndPIN("Up to 60 characters of text here..") - )); -``` - -Example 5: `confirmationMessage` Only (No Fallback) -Description: Insist on `confirmationMessage`; if not available, then fail. -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessage("Up to 200 characters of text here..") -)); -``` - -## Using Semantics Identifier -```java -var client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Create the signable data -var signableData = new SignableData("Test data to sign".getBytes()); -signableData.setHashType(HashType.SHA256); +// Create the signable data +var signableData = new SignableData("Test data to sign".getBytes()); +signableData.setHashType(HashType.SHA256); // Create the Semantics Identifier var semanticsIdentifier = new SemanticsIdentifier( @@ -1206,7 +973,7 @@ String sessionSecret = signatureResponse.getSessionSecret(); // Use the session information as needed ``` -## Using Document Number +#### Initiating a Dynamic Link Signature Session Using Document Number ```java SmartIdClient client = new SmartIdClient(); client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); @@ -1239,17 +1006,19 @@ String sessionSecret = signatureResponse.getSessionSecret(); // Use the session information as needed ``` +Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. +Jump to [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. -## Response Parameters +### Response Parameters The response from a successful dynamic link signature session creation contains the following parameters: * `sessionID`: A string that can be used to request the operation result. * `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -## Error Handling +### Error Handling Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException`, `SessionTimeoutException`, and others. - + ```java try { DynamicLinkSessionResponse response = builder.initSignatureSession(); @@ -1258,82 +1027,481 @@ String sessionID = response.getSessionID(); String sessionToken = response.getSessionToken(); String sessionSecret = response.getSessionSecret(); -System.out.println("Session ID: " + sessionID); -System.out.println("Session Token: " + sessionToken); -System.out.println("Session Secret: " + sessionSecret); +System.out.println("Session ID: " + sessionID); +System.out.println("Session Token: " + sessionToken); +System.out.println("Session Secret: " + sessionSecret); + +} catch (UserAccountNotFoundException e) { +System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { +System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { +System.out.println("The required interactionDeprecated is not supported by the user's app."); +} catch (ServerMaintenanceException e) { +System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { +System.out.println("An error occurred: " + e.getMessage()); +} +``` + +### Additional Information +* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. + +```java +builder.withAllowedInteractionsOrder(List.of( + Interaction.confirmationMessage("Please confirm the transaction of 1024.50 EUR"), + Interaction.displayTextAndPIN("Confirm transaction") +)); +``` + +* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. + +```java +var parameters = new RawDigestSignatureProtocolParameters(); +parameters.setDigest(signableData.calculateHashInBase64()); +parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); +builder.withSignatureProtocolParameters(parameters); +``` + +* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. + +```java +var requestProperties = new RequestProperties(); +requestProperties.setShareMdClientIpAddress(true); +builder.withRequestProperties(requestProperties); +``` + +* `Nonce`: A unique identifier (up to 30 characters) used to manage idempotent behavior in session creation requests. If a request is repeated within a 15-second timeframe, the same session ID may be returned unless a different nonce is provided. + +```java +builder.withNonce("randomNonce123"); +``` + +* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. + +```java +builder.withCapabilities(Set.of("QUILIFIED", "ADVANCED")); +``` + +* `Certificate Level`: Set the required certificate level (`ADVANCED` or `QUALIFIED`). Defaults to `QUALIFIED`. + +```java +builder.withCertificateLevel(CertificateLevel.QUALIFIED); +``` + +### Examples of Allowed Dynamic-link Interactions Order +An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. +For dynamic link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. +Each interaction is defined by an object that includes a type and either displayText60 (for shorter texts) or displayText200 (for longer texts). + +Below are examples of allowedInteractionsOrder elements specifically for dynamic link flows: + +Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. +```java +builder.withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), + DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") + )); +``` + +Example 2: `displayTextAndPIN` Only +Description: Use `displayTextAndPIN` interactionDeprecated only. +```java +builder.withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") +)); +``` + +Example 3: `confirmationMessage` Only (No Fallback) +Description: Insist on `confirmationMessage`; if not available, then fail. +```java +builder.withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here..") +)); +``` + +### Generating QR-code or dynamic link + +#### Generating dynamic link + +Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. +Providing QR-code as a dynamic link type will allow generating QR-code at frontend side. + +#### Dynamic link parameters + +* `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. +* `version`: Version of the dynamic link. Default value is `0.1`. +* `dynamicLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. +* `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. +* `sessionToken`: Token from the session response. +* `elapsedTime`: Elapsed time from when the session response was received. +* `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link +* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType and current time and session secret. Session secret can be found in the session response. + +```java +DynamicLinkSessionResponse response; // response from the session initiation query. +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI dynamicLink = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createUri(); +``` + +#### Overriding default values + +```java +DynamicLinkSessionResponse response; // response from the session initiation query. +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI dynamicLink = client.createDynamicContent() + .withBaseUrl("https://example.com") // override default base URL (https://smart-id.com/dynamic-link) + .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withUserLanguage("est") // override default user language (eng) + .withAuthCode(authCode) + .createUri(); +``` + +#### Generating QR-code + +Creating a QR code uses the Zxing library to generate a QR code image with dynamic link as content. +According to link size the QR-code of version 9 (53x53 modules) is used. +For the QR-code to be scannable by most devices the QR code module size should be 10px. +It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). +Generated QR code will have error correction level low. + +#### Generate QR-code Data URI + +```java +DynamicLinkSessionResponse response; // init auth sessions response +// Capture and store when initiating sessions response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) +String qrCodeDataUri = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createQrCode(); +``` + +#### Generate QR-code with custom height, width, quiet area and image format + +Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. +QR code version 9 (53x53 modules) is automatically selected by content size + +Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. +The width and height of 1159px produce a QR code with a module size of 19px. + +```java +DynamicLinkSessionResponse response; // init auth sessions response +// Capture and store when initiating session response arrived +Instant responseReceivedTime = Instant.now(); +// Generate auth code +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +// Generate dynamic link +URI qrDataUri = client.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for, possible values (auth, sign, cert) + .withSessionToken(response.getSessionToken()) // provide token from sessions response + .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createUri(); + +// Generate QR-code with height and width of 570px and quiet area of 2 modules. +BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(qrDataUri, 570, 570, 2); + +// Convert BufferedImage to Data URI +String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); +``` + +## Session status request handling for v3.0 + +The Smart-ID v3.0 API includes new session status request paths for retrieving session results. + +### Session status response + +The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: + +* `state`: RUNNING or COMPLETE +* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) +* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. +* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature sessions) +* `signature`: Contains the following fields based on the signatureProtocol used: + * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm + * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm +* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). +* `ignoredProperties`: Any unsupported or ignored properties from the request. +* `interactionFlowUsed`: The interactionDeprecated flow used for the session. +* `deviceIpAddress`: IP address of the mobile device, if requested. + +### Example of fetching session status in v3.0 + +#### Example of using session status poller to query final sessions status +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Client setup with TrustStore. Requests will not work without a valid certificate. +InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); +client.setTrustStore(trustStore); + +// +SessionsStatusPoller poller = client.getSessionsStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); + +if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); +} +``` + +#### Example of querying sessions status +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + +// Client setup with TrustStore. Requests will not work without a valid certificate. +InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); +client.setTrustStore(trustStore); + +// Get the session status poller +SessionsStatusPoller poller = client.getSessionsStatusPoller(); + +// Queryinn +SessionStatus sessionStatus = poller.getSessionsStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); +if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + // Session is still running and querying can be continued + // Dynamic content can be generated and displayed to the user +} else { + // continue to the next step +} +``` + +### Validating session status response + +It's important to validate the session status response to ensure that the returned signature or authentication result is valid. + +* Validate that endResult is OK if the session was successful. +* Check the certificate field to ensure it has the required certificate level and that it is signed by a trusted CA. +* For `ACSP_V1` signature validation, compare the digest of the signature protocol, server random, and random challenge. +* For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. + +#### Example of validating the authentication sessions response: + +```java +// init authentication response validator with trusted certificates +// there are 4 different ways to initialize the validator +// 1. use default values `trusted_certificates.jks` with password `changeit` +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); +// 2. provide your own path to truststore and truststore password +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(truststorePath, truststorePassword); +// 3 read trusted certificate yourself and provide it to the validator +X509Certificate[] trustedCertificates = findTrustedCertificates(); +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCertificates); +// 4. init authentication response validator with the empty array and add trusted certificates +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[0]); +X509Certificate certificate = getTrustedCertificate(); +authenticationResponseValidator.addTrustedCACertificate(certificate); + +// get sessions result +SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); + +// validate sessions state is completed +if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + // validate sessions status result and map session status to authentication response + DynamicLinkAuthenticationResponse response = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.from(response, "randomChallenge"); +} +``` + +#### Example of validating the signature: + +```java +try { + // Map and validate the session status + SignatureResponse signatureResponse = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); + + // Process the response (e.g., save to database or pass to another system) + handleSignatureResponse(signatureResponse); + +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); +} +``` + +### Error handling for session status + +The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: + +* `OK`: Session completed successfully. +* `USER_REFUSED`: User refused the session. +* `TIMEOUT`: User did not respond in time. +* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. +* `WRONG_VC`: User selected the wrong verification code. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interactionDeprecated is not supported by the user's app. +* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. +* `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). +* `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. +* `USER_REFUSED_CONFIRMATIONMESSAGE`: User cancelled on confirmationMessage screen. +* `USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE`: User cancelled on confirmationMessageAndVerificationCodeChoice screen. + +## Notification-based flows + +### Differences Between Notification-Based and Dynamic Link Flows +* `Notification-Based flow` + * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. + * Known users or devices: Suitable when the RP already knows the user's identity or device. + * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. +* `Dynamic Link flow` + * Dynamic links: Generates links like QR codes or Web2App/App2App links that the user interacts with to start the process. + * Supports unknown users or devices: Useful when the user's identity or device is not known in advance. + * Real-time updates: Dynamic links need to be refreshed every second to ensure validity, especially for QR codes. + +### Examples of performing notification authentication + +#### Initiating notification authentication session with document number +```java +String documentNumber = "PNOLT-30303039914-MOCK-Q"; + +String randomChallenge = RandomChallenge.generate(); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); + +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String verificationCode = authenticationSessionResponse.getVc().getValue(); +// Display the verification code to the user for confirmation +``` +After initiating the session, display the verificationCode to the user. The user must confirm that the code displayed in their Smart-ID app matches the one you have provided. + +#### Initiating notification authentication session with semantics identifier +Alternatively, you can initiate a notification authentication session using a semantics identifier, which uniquely identifies the user across different countries and identity types. +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "30303039914" +); -} catch (UserAccountNotFoundException e) { -System.out.println("User account not found."); -} catch (RelyingPartyAccountConfigurationException e) { -System.out.println("Relying party account configuration issue."); -} catch (RequiredInteractionNotSupportedByAppException e) { -System.out.println("The required interactionDeprecated is not supported by the user's app."); -} catch (ServerMaintenanceException e) { -System.out.println("Server maintenance in progress, please try again later."); -} catch (SmartIdClientException e) { -System.out.println("An error occurred: " + e.getMessage()); -} -``` +String randomChallenge = RandomChallenge.generate(); -## Additional Information -* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + Interaction.verificationCodeChoice("Log in to self-service?") + )) + .initAuthenticationSession(); -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessage("Please confirm the transaction of 1024.50 EUR"), - Interaction.displayTextAndPIN("Confirm transaction") -)); +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later + +String verificationCode = authenticationSessionResponse.getVc().getValue(); +// Display the verification code to the user for confirmation ``` +Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. +Jump to [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. -* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. +### Initiating a Notification Certificate Choice Session -```java -var parameters = new RawDigestSignatureProtocolParameters(); -parameters.setDigest(signableData.calculateHashInBase64()); -parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); -builder.withSignatureProtocolParameters(parameters); -``` +#### Request Parameters +The request parameters for the dynamic link certificate choice session are: -* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. +* `relyingPartyUUID`: UUID of the Relying Party. +* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. +#### Initiating a Notification Certificate Choice Using Semantics Identifier ```java -var requestProperties = new RequestProperties(); -requestProperties.setShareMdClientIpAddress(true); -builder.withRequestProperties(requestProperties); -``` +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "30303039914"); // identifier (according to country and identity type reference) -* `Nonce`: A unique identifier (up to 30 characters) used to manage idempotent behavior in session creation requests. If a request is repeated within a 15-second timeframe, the same session ID may be returned unless a different nonce is provided. +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withNonce("1234567890") + .initCertificateChoice(); -```java -builder.withNonce("randomNonce123"); +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later ``` -* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. - +#### Initiating a Notification Certificate Choice Using Document Number ```java -builder.withCapabilities(Set.of("QUILIFIED", "ADVANCED")); -``` +String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication -* `Certificate Level`: Set the required certificate level (`ADVANCED` or `QUALIFIED`). Defaults to `QUALIFIED`. + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withNonce("1234567890") + .initCertificateChoice(); -```java -builder.withCertificateLevel(CertificateLevel.QUALIFIED); +String sessionId = authenticationSessionResponse.getSessionID(); +// SessionID is used to query sessions status later ``` -# Initiating a Notification-Based Signature Session in API v3.0 +### Initiating a Notification-Based Signature Session The Smart-ID API v3.0 allows you to initiate a signature session using a notification-based flow. This method is useful when the user is already known or authenticated, and you want to initiate the signing process directly through a notification to the user's device, without the need for a dynamic link. -## Differences Between Notification-Based and Dynamic Link Flows -* `Notification-Based flow` - * The user receives a notification on their Smart-ID app to complete the signing process. - * Suitable for scenarios where the user's identity or device is already known. - * Uses different interactionDeprecated types compared to dynamic link flows. -* `Dynamic Link flow` - * Generates a dynamic link that the user must access to initiate the signing process. - * Useful when the user's identity or device is not known beforehand. - -## Request Parameters +#### Request Parameters The request parameters for the notification-based signature session are as follows: * `relyingPartyUUID`: Required. UUID of the Relying Party. @@ -1352,10 +1520,10 @@ The request parameters for the notification-based signature session are as follo * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -## Example: Initiating a Notification-Based Signature Request +#### Example: Initiating a Notification-Based Signature Request Below is an example of how to initiate a notification-based signature request using the Smart-ID Java client, using both the Semantics Identifier and Document Number endpoints. -## Using Semantics Identifier +#### Initiating a Notification-Based Signature Session Using Semantics Identifier ```java SmartIdClient client = new SmartIdClient(); client.setRelyingPartyUUID("your-relying-party-uuid"); @@ -1396,7 +1564,7 @@ System.out.println("Verification Code Type: " + verificationCode.getType()); System.out.println("Verification Code Value: " + verificationCode.getValue()); ``` -## Using Document Number +#### Initiating a Notification-Based Signature Session Using Document Number ```java SmartIdClient client = new SmartIdClient(); client.setRelyingPartyUUID("your-relying-party-uuid"); @@ -1432,39 +1600,19 @@ System.out.println("Session ID: " + sessionID); System.out.println("Verification Code Type: " + verificationCode.getType()); System.out.println("Verification Code Value: " + verificationCode.getValue()); ``` +Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. +Jump to [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. -## Examples of Allowed Interactions Order -In notification-based flows, the available interactionDeprecated types differ from those in dynamic link flows. Below are the interactionDeprecated types allowed in notification-based flows: - -* `verificationCodeChoice` with `displayText60` -* `confirmationMessageAndVerificationCodeChoice` with `displayText200` - -### Example 1: confirmationMessageAndVerificationCodeChoice with Fallback to verificationCodeChoice -```java -NotificationSignatureSessionRequestBuilder builder = new NotificationSignatureSessionRequestBuilder(connector) - .withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessageAndVerificationCodeChoice("Confirm transaction of 1024.50 EUR"), - Interaction.verificationCodeChoice("Confirm transaction") - )); -``` - -### Example 2: verificationCodeChoice Only -```java -NotificationSignatureSessionRequestBuilder builder = new NotificationSignatureSessionRequestBuilder(connector) - .withAllowedInteractionsOrder(List.of(Interaction.verificationCodeChoice("Confirm transaction") - )); -``` - -## Response on Successful Notification-based Signature Session Creation +#### Response on Successful Notification-based Signature Session Creation Upon successful initiation, the user will receive a notification on their Smart-ID app to complete the signing process. The response includes the `sessionID` and a `verificationCode` (Verification Code) object. -## Response Parameters +### Response Parameters * `sessionID`: Required. String used to request the operation result. * `verificationCode`: Required. Object describing the Verification Code to be displayed. - * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. - * `value`: Required. Value of the VC code. + * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. + * `value`: Required. Value of the VC code. -## Error Handling +### Error Handling Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: * `UserAccountNotFoundException` * `RelyingPartyAccountConfigurationException` @@ -1498,7 +1646,7 @@ try { } ``` -## Additional Information +### Additional Information * `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. For notification-based flows, use `verificationCodeChoice` and `confirmationMessageAndVerificationCodeChoice`. ```java builder.withAllowedInteractionsOrder(List.of( @@ -1593,61 +1741,29 @@ SmartIdSignature signature = SmartIdSignature.fromSessionStatus(sessionStatus); // Use the signature as needed ``` -## Examples of performing notification authentication - -### Initiating notification authentication session with document number -```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; - -String randomChallenge = RandomChallenge.generate(); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .initAuthenticationSession(); +### Examples of Allowed Notification-based Interactions Order +An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. +Different interactions can support different amounts of data to display information to the user. -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +Below are examples of `allowedInteractionsOrder` elements in JSON format: -String verificationCode = authenticationSessionResponse.getVc().getValue(); -// Display the verification code to the user for confirmation +Example 1: `verificationCodeChoice` only +Description: Use `verificationCodeChoice` interaction exclusively. +```java +builder.withAllowedInteractionsOrder(List.of( + NotificationInteraction.verificationCodeChoice("Up to 60 characters of text here...") + )); ``` -After initiating the session, display the verificationCode to the user. The user must confirm that the code displayed in their Smart-ID app matches the one you have provided. -### Initiating notification authentication session with semantics identifier -Alternatively, you can initiate a notification authentication session using a semantics identifier, which uniquely identifies the user across different countries and identity types. +Example 2: `confirmationMessageAndVerificationCodeChoice` only +Description: Insist on `confirmationMessageAndVerificationCodeChoice`; if not available, then fail. ```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "30303039914" -); - -String randomChallenge = RandomChallenge.generate(); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .initAuthenticationSession(); - -String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later - -String verificationCode = authenticationSessionResponse.getVc().getValue(); -// Display the verification code to the user for confirmation +builder.withAllowedInteractionsOrder(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") + )); ``` -### Requesting the IP Address of the User's Device +## Requesting the IP Address of the User's Device If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withShareMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. ```java NotificationAuthenticationSessionResponse authenticationSessionResponse = client @@ -1662,114 +1778,31 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client .initAuthenticationSession(); ``` -### Generating QR-code or dynamic link -Todo: will be implemented in task SLIB-55 -## Generating QR-code or dynamic link - -#### Generating dynamic link - -Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. -Providing QR-code as a dynamic link type will allow generating QR-code at frontend side. - -##### Dynamic link parameters - -* `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. -* `version`: Version of the dynamic link. Default value is `0.1`. -* `dynamicLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. -* `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. -* `sessionToken`: Token from the session response. -* `elapsedTime`: Elapsed time from when the session response was received. -* `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link -* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType and current time and session secret. Session secret can be found in the session response. - -```java -DynamicLinkAuthenticationSessionResponse response; // response from the session initiation query. -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); -// Generate dynamic link -URI dynamicLink = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link - .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createUri(); -``` - -##### Overriding default values - -```java -DynamicLinkAuthenticationSessionResponse response; // response from the session initiation query. -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); -// Generate dynamic link -URI dynamicLink = client.createDynamicContent() - .withBaseUrl("https://example.com") // override default base URL (https://smart-id.com/dynamic-link) - .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time - .withUserLanguage("est") // override default user language (eng) - .withAuthCode(authCode) - .createUri(); -``` - -#### Generating QR-code - -Creating a QR code uses the Zxing library to generate a QR code image with dynamic link as content. -According to link size the QR-code of version 9 (53x53 modules) is used. -For the QR-code to be scannable by most devices the QR code module size should be 10px. -It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). -Generated QR code will have error correction level low. - -##### Generate QR-code Data URI - -```java -DynamicLinkAuthenticationSessionResponse response; // init auth sessions response -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); -// Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) -String qrCodeDataUri = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createQrCode(); -``` - -##### Generate QR-code with custom height, width, quiet area and image format - -Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. -QR code version 9 (53x53 modules) is automatically selected by content size - -Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. -The width and height of 1159px produce a QR code with a module size of 19px. - -```java -DynamicLinkAuthenticationSessionResponse response; // init auth sessions response -// Capture and store when initiating session response arrived -Instant responseReceivedTime = Instant.now(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); -// Generate dynamic link -URI qrDataUri = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for, possible values (auth, sign, cert) - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createUri(); - -// Generate QR-code with height and width of 570px and quiet area of 2 modules. -BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(qrDataUri, 570, 570, 2); - -// Convert BufferedImage to Data URI -String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); -``` \ No newline at end of file +## Exception Handling +The Smart-ID Java client library provides specific exceptions for different error scenarios. Handle exceptions appropriately to provide a good user experience. + +Exception Categories +* Permanent Exceptions + These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input + * `SmartIdClientException` Thrown for general client-side errors, such as: + * Missing or invalid configuration (e.g., `trustSslContext` not set). + * Unexpected response data (e.g., missing required fields in session status.) +* Unprocessable Response Exceptions + These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. + * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. + * Missing required fields (e.g., `state`, `endResult`, `signatureAlgorithm`). + * Incorrectly encoded Base64 strings in signature or certificate. + * Unexpected or unsupported `signatureProtocol`. +* User Action Exceptions + These exceptions cover scenarios where user actions or inactions lead to session termination or errors. + * `UserRefusedException` Base exception for user refusal scenarios. + * `SessionTimeoutException`: User did not respond within the allowed timeframe. + * `UserSelectedWrongVerificationCodeException` Thrown when the user selects an incorrect verification code during the process. +* User Account Exceptions + These exceptions handle issues related to the user's Smart-ID account or session requirements. + * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. + * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. +* Validation and Parsing Exceptions + These exceptions arise during validation or parsing operations within the library. + * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. + * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java index 10574b9d..4d35056d 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java @@ -132,6 +132,10 @@ private static void validateSessionResult(SessionStatus sessionStatus, String re throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); } + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); + } + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); validateSignature(sessionStatus); } else { From b2d772eed1cebf0d884aa21d7da31444ba0c2535 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 28 Mar 2025 09:19:13 +0200 Subject: [PATCH 16/57] Usability changes from testing and refactoring dynamic-link authentication mapper (#101) * SLIB-69 - add receivedAt as a read-only field to DynamicLinkSessionResponse * SLIB-69 - fix notification-based certificate choice query to ignore unknown fields; add unknown fields to v3 test responses * SLIB-69 - add integration tests for SmartIdRestConnector * SLIB-69 - change dynamicLinkAuthenticationResponse with mapper and validators to be used for notification-based authentication * SLIB-69 - add ReadmeIntegrationTest.java for authentication flows; update ReadMe * SLIB-69 - add Readme test for certificate choice; fix typos; update Readme * SLIB-69 - replace certificate in SignatureResponseMapperTest * SLIB-69 - add certificate choice response mapper * SLIB-69 - add Readme tests for signature session; update Readme; fix typos; refactoring test code * SLIB-69 - move v2 ReadmeTest to v2 test package * SLIB-69 - add license header for new files * SLIB-69 - code review fixes * SLIB-69 - update session status tests * SLIB-69 - update years in license header copyright section * SLIB-69 - update changelog * SLIB-69 - fix mock sessions status response change issue --- CHANGELOG.md | 22 +- README.md | 1046 ++++++++--------- .../CertificateLevelMismatchException.java | 8 +- .../util/CertificateAttributeUtil.java | 6 +- .../util/NationalIdentityNumberUtil.java | 2 +- .../java/ee/sk/smartid/util/StringUtil.java | 2 +- .../ee/sk/smartid/v2/AuthenticationHash.java | 2 +- .../v2/AuthenticationRequestBuilder.java | 2 +- .../v2/AuthenticationResponseValidator.java | 2 +- .../ee/sk/smartid/v2/CertificateLevel.java | 2 +- .../smartid/v2/CertificateRequestBuilder.java | 2 +- .../java/ee/sk/smartid/v2/SignableData.java | 2 +- .../java/ee/sk/smartid/v2/SignableHash.java | 2 +- .../smartid/v2/SignatureRequestBuilder.java | 2 +- .../v2/SmartIdAuthenticationResponse.java | 2 +- .../ee/sk/smartid/v2/SmartIdCertificate.java | 2 +- .../java/ee/sk/smartid/v2/SmartIdClient.java | 2 +- .../sk/smartid/v2/SmartIdRequestBuilder.java | 2 +- .../ee/sk/smartid/v2/SmartIdSignature.java | 2 +- .../v2/VerificationCodeCalculator.java | 2 +- .../smartid/v2/rest/SessionStatusPoller.java | 2 +- .../sk/smartid/v2/rest/SmartIdConnector.java | 2 +- .../smartid/v2/rest/SmartIdRestConnector.java | 2 +- .../dao/AuthenticationSessionRequest.java | 2 +- .../dao/AuthenticationSessionResponse.java | 2 +- .../ee/sk/smartid/v2/rest/dao/Capability.java | 2 +- .../rest/dao/CertificateChoiceResponse.java | 2 +- .../v2/rest/dao/CertificateRequest.java | 2 +- .../sk/smartid/v2/rest/dao/Interaction.java | 2 +- .../smartid/v2/rest/dao/InteractionFlow.java | 2 +- .../v2/rest/dao/RequestProperties.java | 2 +- .../v2/rest/dao/SessionCertificate.java | 2 +- .../sk/smartid/v2/rest/dao/SessionResult.java | 2 +- .../smartid/v2/rest/dao/SessionSignature.java | 2 +- .../sk/smartid/v2/rest/dao/SessionStatus.java | 2 +- .../v2/rest/dao/SessionStatusRequest.java | 2 +- .../v2/rest/dao/SignatureSessionRequest.java | 2 +- .../v2/rest/dao/SignatureSessionResponse.java | 2 +- ...ponse.java => AuthenticationResponse.java} | 4 +- ...java => AuthenticationResponseMapper.java} | 36 +- .../v3/AuthenticationResponseValidator.java | 72 +- .../smartid/v3/CertificateChoiceResponse.java | 90 ++ .../v3/CertificateChoiceResponseMapper.java | 137 +++ .../ee/sk/smartid/v3/CertificateLevel.java | 6 +- ...ertificateChoiceSessionRequestBuilder.java | 8 +- .../java/ee/sk/smartid/v3/SignableHash.java | 4 +- ...reResponse.java => SignatureResponse.java} | 23 +- .../smartid/v3/SignatureResponseMapper.java | 39 +- .../java/ee/sk/smartid/v3/SmartIdClient.java | 8 +- .../smartid/v3/rest/SessionStatusPoller.java | 6 +- .../sk/smartid/v3/rest/SmartIdConnector.java | 6 +- .../smartid/v3/rest/SmartIdRestConnector.java | 2 +- .../rest/dao/DynamicLinkSessionResponse.java | 15 +- ...ationCertificateChoiceSessionResponse.java | 3 + .../v3/rest/dao/NotificationInteraction.java | 18 +- .../ee/sk/smartid/CertificateParserTest.java | 3 +- .../java/ee/sk/smartid/SignableDataTest.java | 2 +- .../sk/smartid/SmartIdRestServiceStubs.java | 2 +- .../v2/AuthenticationIdentityTest.java | 2 +- .../v2/AuthenticationRequestBuilderTest.java | 8 +- .../AuthenticationResponseValidatorTest.java | 2 +- .../sk/smartid/v2/CertificateLevelTest.java | 4 +- .../v2/CertificateRequestBuilderTest.java | 6 +- .../ee/sk/smartid/v2/CertificateUtil.java | 2 +- .../smartid/v2/ClientRequestHeaderFilter.java | 2 +- src/test/java/ee/sk/smartid/v2/DummyData.java | 2 +- ...ndpointSslVerificationIntegrationTest.java | 4 +- .../ee/sk/smartid/v2/SignableHashTest.java | 3 +- .../v2/SignatureRequestBuilderTest.java | 8 +- .../v2/SmartIdAuthenticationResponseTest.java | 2 +- .../ee/sk/smartid/v2/SmartIdClientTest.java | 6 +- .../sk/smartid/v2/SmartIdSignatureTest.java | 3 +- .../v2/VerificationCodeCalculatorTest.java | 2 +- .../{ => v2}/integration/ReadmeTest.java | 22 +- .../integration/SmartIdIntegrationTest.java | 12 +- .../v2/rest/SessionStatusPollerTest.java | 2 +- .../smartid/v2/rest/SmartIdConnectorSpy.java | 2 +- .../v2/rest/SmartIdRestConnectorTest.java | 10 +- .../v2/rest/SmartIdRestIntegrationTest.java | 8 +- .../v2/rest/dao/SemanticsIdentifierTest.java | 2 +- .../rest/dao/SignatureSessionRequestTest.java | 2 +- .../v2/util/CertificateAttributeUtilTest.java | 4 +- .../util/NationalIdentityNumberUtilTest.java | 4 +- ... => AuthenticationResponseMapperTest.java} | 52 +- .../AuthenticationResponseValidatorTest.java | 30 +- .../CertificateChoiceResponseMapperTest.java | 243 ++++ ...ficateChoiceSessionRequestBuilderTest.java | 33 +- ...inkSignatureSessionRequestBuilderTest.java | 2 +- .../sk/smartid/v3/ErrorResultHandlerTest.java | 36 +- ...essionEndResultErrorArgumentsProvider.java | 65 + .../v3/SignatureResponseMapperTest.java | 49 +- .../ee/sk/smartid/v3/SmartIdClientTest.java | 55 +- .../v3/integration/ReadmeIntegrationTest.java | 763 ++++++++++++ .../SmartIdRestIntegrationTest.java | 322 +++++ .../v3/rest/SessionStatusPollerTest.java | 10 +- .../v3/rest/SmartIdRestConnectorTest.java | 320 +++-- .../v3/rest/SmartIdRestIntegrationTest.java | 95 -- .../cert-choice-cert-40504040001.pem.cert | 42 + .../test-certs/sign-cert-40504040001.pem.crt | 42 + src/test/resources/trusted_certificates.jks | Bin 3585 -> 5684 bytes ...-link-authentication-session-response.json | 3 +- ...amic-link-certificate-choice-response.json | 5 - ...k-certificate-choice-session-response.json | 6 + .../dynamic-link-signature-response.json | 5 - ...namic-link-signature-session-response.json | 6 + ...n-certificate-choice-session-response.json | 3 +- .../notification-session-response.json | 4 +- .../session-status-document-unusable.json | 8 + .../v3/responses/session-status-not-ok.json | 6 - .../v3/responses/session-status-ok.json | 6 - ...tatus-running-with-ignored-properties.json | 5 + ...sion-status-successful-authentication.json | 22 + ...-status-successful-certificate-choice.json | 14 + .../session-status-successful-signature.json | 21 + .../v3/responses/session-status-timeout.json | 8 + ...ssion-status-user-refused-cert-choice.json | 8 + ...s-user-refused-confirmation-vc-choice.json | 8 + ...sion-status-user-refused-confirmation.json | 8 + ...tus-user-refused-display-text-and-pin.json | 8 + ...session-status-user-refused-vc-choice.json | 8 + .../session-status-user-refused.json | 8 + .../v3/responses/session-status-wrong-vc.json | 8 + 122 files changed, 2902 insertions(+), 1193 deletions(-) rename src/main/java/ee/sk/smartid/v3/{DynamicLinkAuthenticationResponse.java => AuthenticationResponse.java} (97%) rename src/main/java/ee/sk/smartid/v3/{DynamicLinkAuthenticationResponseMapper.java => AuthenticationResponseMapper.java} (80%) create mode 100644 src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java create mode 100644 src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java rename src/main/java/ee/sk/smartid/v3/{SingatureResponse.java => SignatureResponse.java} (87%) rename src/test/java/ee/sk/smartid/{ => v2}/integration/ReadmeTest.java (99%) rename src/test/java/ee/sk/smartid/{ => v2}/integration/SmartIdIntegrationTest.java (99%) rename src/test/java/ee/sk/smartid/v3/{DynamicLinkAuthenticationResponseMapperTest.java => AuthenticationResponseMapperTest.java} (85%) create mode 100644 src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java create mode 100644 src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java create mode 100644 src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java delete mode 100644 src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java create mode 100644 src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert create mode 100644 src/test/resources/test-certs/sign-cert-40504040001.pem.crt delete mode 100644 src/test/resources/v3/responses/dynamic-link-certificate-choice-response.json create mode 100644 src/test/resources/v3/responses/dynamic-link-certificate-choice-session-response.json delete mode 100644 src/test/resources/v3/responses/dynamic-link-signature-response.json create mode 100644 src/test/resources/v3/responses/dynamic-link-signature-session-response.json create mode 100644 src/test/resources/v3/responses/session-status-document-unusable.json delete mode 100644 src/test/resources/v3/responses/session-status-not-ok.json delete mode 100644 src/test/resources/v3/responses/session-status-ok.json create mode 100644 src/test/resources/v3/responses/session-status-running-with-ignored-properties.json create mode 100644 src/test/resources/v3/responses/session-status-successful-authentication.json create mode 100644 src/test/resources/v3/responses/session-status-successful-certificate-choice.json create mode 100644 src/test/resources/v3/responses/session-status-successful-signature.json create mode 100644 src/test/resources/v3/responses/session-status-timeout.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused-cert-choice.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused-confirmation-vc-choice.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused-confirmation.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused-display-text-and-pin.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused-vc-choice.json create mode 100644 src/test/resources/v3/responses/session-status-user-refused.json create mode 100644 src/test/resources/v3/responses/session-status-wrong-vc.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ef29b3b..0ebfcbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 package. - - New builder classes to start dynamic-link sessions: + - New builder classes to start v3 sessions: - DynamicLinkAuthenticationSessionRequestBuilder - DynamicLinkCertificateChoiceSessionRequestBuilder - DynamicLinkSignatureSessionRequestBuilder - - Helper class for generating authCode used in generating dynamic link - AuthCode#generateAuthCode() - - Helper class for generating Qr-code - QrCodeGenerator - - Helper class for building and generating dynamic-link and/or QR-code - DynamicContentBuilder - - Sessions status request handling for the v3 path. - - Helper class for validating completed auth session status response - DynamicLinkAuthenticationResponseMapper - - Constructing AuthenticationIdentity from DynamicLinkAuthenticationResponse - AuthenticationResponseValidator + - NotificationAuthenticationSessionRequestBuilder + - NotificationCertificateChoiceSessionRequestBuilder + - NotificationSignatureSessionRequestBuilder + - Helper class for dynamic link + - AuthCode - used for generating authCode necessary for dynamic-link + - QrCodeGenerator - to create QR-code from dynamic-link + - DynamicContentBuilder - to create dynamic link or QR-code + - Support for sessions status request handling for the v3 path. + - Added AuthenticationResponseMapper for validating required fields and mapping session status to authentication response + - Added AuthenticationResponseValidator to validate certificate and signed authentication response and construct AuthenticationIdentity + - Added SignatureResponseMapper for validating required fields and mapping session status to signature response + - Added CertificateChoiceResponseMapper for validating required fields and mapping session status to certificate choice response ### Changed - Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. @@ -28,7 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Removed deprecated methods from AuthenticationIdentity ### Java and dependency updates -- Updated java to version 17 +- Updated minimal supported java to version 17 - Updated slf4j-api to version 2.0.16 - Updated jackson dependencies to version 2.17.2 - Added jakarta.ws.rs:jakarta.ws.rs-api diff --git a/README.md b/README.md index a2799008..b2cd25cd 100644 --- a/README.md +++ b/README.md @@ -47,18 +47,21 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Configuring a proxy using Jersey](#configuring-a-proxy-using-jersey) * [How to use it with RP API v3.0](#how-to-use-api-v30) * [Setting up SmartIdClient for v3.0](#setting-up-smartidclient-for-v30) - * [Dynamic Link flows](#dynamic-link-flows) - * [Initiating authentication session](#examples-of-performing-dynamic-link-authentication) - * [Initiating anonymous authentication session](#initiating-anonymous-authentication-session) - * [Initiating authentication session with semantics identifier](#initiating-authentication-session-with-semantics-identifier) - * [Initiating authentication session with document number](#initiating-authentication-session-with-document-number) - * [Initiating a Dynamic Link Certificate Choice Session](#initiating-a-dynamic-link-certificate-choice-session) - * [Example of Initiating a Dynamic Link Certificate Choice Request](#example-initiating-a-dynamic-link-certificate-choice-request) - * [Response on Successful Certificate Choice Session Creation](#response-on-successful-certificate-choice-session-creation) - * [Initiating a Dynamic Link Signature Session](#initiating-a-dynamic-link-signature-session) - * [Initiating a Dynamic Link Signature Session Using Semantics Identifier](#initiating-a-dynamic-link-signature-session-using-semantics-identifier) - * [Initiating a Dynamic Link Signature Session Using Document Number](#initiating-a-dynamic-link-signature-session-using-document-number) - * [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) + * [Dynamic link flows](#dynamic-link-flows) + * [Dynamic link authentication session](#dynamic-link-authentication-session) + * [Examples of authentication session](#examples-of-initiating-a-dynamic-link-authentication-session) + * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) + * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-dynamic-link-authentication-session-with-semantics-identifier) + * [Initiating a dynamic-link authentication session with document number](#initiating-a-dynamic-link-authentication-session-with-document-number) + * [Dynamic link certificate choice session](#dynamic-link-certificate-choice-session) + * [Examples of initiating a dynamic-link certificate choice session](#examples-of-initiating-a-dynamic-link-certificate-choice-session) + * [Initiating dynamic-link certificate choice](#initiating-an-anonymous-certificate-choice-session) + * [Dynamic-link signature session](#dynamic-link-signature-session) + * [Examples of initiating a dynamic-link signature session](#examples-of-initiating-a-notification-based-signature-session) + * [Initiating a dynamic-link signature session using semantics identifier](#initiating-a-dynamic-link-signature-session-with-semantics-identifier) + * [Initiating a dynamic-link signature session using document number](#initiating-a-dynamic-link-signature-session-with-document-number) + * [Examples of allowed dynamic-link interactions order](#examples-of-allowed-dynamic-link-interactions-order) + * [Additional request properties](#additional-dynamic-link-session-request-properties) * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) * [Generating dynamic link ](#generating-dynamic-link) * [Dynamic link parameters](#dynamic-link-parameters) @@ -68,28 +71,30 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) * [Querying sessions status](#session-status-request-handling-for-v30) * [Sessions status response](#session-status-response) - * [Example of fetching session status in v3.0](#example-of-fetching-session-status-in-v30) + * [Example of querying session status in v3.0](#examples-of-querying-session-status-in-v30) * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) - * [Example of querying sessions status](#example-of-querying-sessions-status) + * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) * [Validating sessions status response](#validating-session-status-response) * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) - * [Example of validating the signature](#example-of-validating-the-signature) + * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) + * [Example of validating the signature](#example-of-validating-the-signature-session-response) * [Error handling for session status](#error-handling-for-session-status) * [Notification-based flows](#notification-based-flows) - * [Differences Between Notification-Based and Dynamic Link Flows](#differences-between-notification-based-and-dynamic-link-flows) - * [Examples of performing notification authentication](#examples-of-performing-notification-authentication) - * [Initiating notification authentication session with document number](#initiating-notification-authentication-session-with-document-number) - * [Initiating notification authentication session with semantics identifier](#initiating-notification-authentication-session-with-semantics-identifier) - * [Initiating a Notification Certificate Choice Session](#initiating-a-notification-certificate-choice-session) - * [Initiating a Notification Certificate Choice Using Semantics Identifier](#initiating-a-notification-certificate-choice-using-semantics-identifier) - * [Initiating a Notification Certificate Choice Using Document Number](#initiating-a-notification-certificate-choice-using-document-number) - * [Initiating a Notification-Based Signature Session](#initiating-a-notification-based-signature-session) - * [Initiating a Notification-Based Signature Session Using Semantics Identifier](#initiating-a-notification-based-signature-session-using-semantics-identifier) - * [Initiating a Notification-Based Signature Session Using Document Number](#initiating-a-notification-based-signature-session-using-document-number) - * [Response on Successful Notification-based Signature Session Creation](#response-on-successful-notification-based-signature-session-creation) - * [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) - * [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) - * [Exception Handling](#exception-handling) + * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-dynamic-link-flows) + * [Notification-based authentication session](#notification-based-authentication-session) + * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) + * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) + * [Initiating notification authentication session with semantics identifier](#initiating-a-notification-based-authentication-session-with-semantics-identifier) + * [Notification-based certificate choice session](#notification-based-certificate-choice-session) + * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) + * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) + * [Initiating notification certificate choice with document number](#initiating-a-notification-based-authentication-session-with-document-number) + * [Notification-based signature session](#notification-based-signature-session) + * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) + * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) + * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) + * [Examples of allowed notification-based interactions order](#examples-of-allowed-notification-based-interactions-order) + * [Exception handling](#exception-handling) ## Introduction @@ -126,12 +131,6 @@ You can use the library as a Maven dependency from the [Maven Central](https://s Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) -In this version, the existing code has been moved into the ee.sk.smartid.v2 package for clarity. This is a breaking change for current users of the library. -To update your application: -Change your import statements from ee.sk.smartid.* to ee.sk.smartid.v2.* -Update any references to classes, methods, or packages accordingly. -Support for Smart-ID API v3.0 has been added in the ee.sk.smartid.v3 package. Documentation for v3.0 is currently limited as it is in the early stages of development. - # How to use API v2.0 ## Test accounts for testing @@ -370,7 +369,7 @@ This may trigger a notification to all the user's devices if user has more than All Smart-ID devices support displaying text that is up to 60 characters long. Some devices also support displaying text (on a separate screen) that is up to 200 characters long -as well as other interactionDeprecated flows like user needs to choose the correct code from 3 different verification codes. +as well as other interaction flows like user needs to choose the correct code from 3 different verification codes. You can send different interactions to user's device and it picks the first one that the app can handle. @@ -403,35 +402,35 @@ SmartIdSignature smartIdSignature = client byte[] signature = smartIdSignature.getValue(); -smartIdSignature.getInteractionFlowUsed(); // which interactionDeprecated was used +smartIdSignature.getInteractionFlowUsed(); // which interaction was used ``` -# Setting the order of preferred interactions for displaying text and asking PIN +## Setting the order of preferred interactions for displaying text and asking PIN -The app can support different interactionDeprecated flows and a Relying Party can demand a particular flow with or without a fallback possibility. -Different interactionDeprecated flows can support different amount of data to display information to user. +The app can support different interaction flows and a Relying Party can demand a particular flow with or without a fallback possibility. +Different interaction flows can support different amount of data to display information to user. Available interactions: -* `displayTextAndPIN` with `displayText60`. The simplest interactionDeprecated with max 60 chars of text and PIN entry on a single screen. Every app has this interactionDeprecated available. +* `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. * `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. * `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. * `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. -The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interactionDeprecated supported by the app is taken from `allowedInteractionsOrder` list and sent to client. -The interactionDeprecated that was actually used is reported back to RP with interactionUsed response parameter to the session request. -If the app cannot support any interactionDeprecated requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. +The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interaction supported by the app is taken from `allowedInteractionsOrder` list and sent to client. +The interaction that was actually used is reported back to RP with interactionFlowUsed response parameter to the session request. +If the app cannot support any interaction requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. `displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. -## Parameter allowedInteractionsOrder most common examples +### Parameter allowedInteractionsOrder most common examples Following allowedInteractionsOrder combinations are most likely to be used. -### Short confirmation message with PIN +#### Short confirmation message with PIN If confirmation message fits to 60 characters then this is the most common choice. -Every Smart-ID app supports this interactionDeprecated flow and there is no need to provide any fallbacks to this interactionDeprecated. +Every Smart-ID app supports this interaction flow and there is no need to provide any fallbacks to this interaction. ```java SmartIdSignature smartIdSignature = client @@ -445,7 +444,7 @@ SmartIdSignature smartIdSignature = client .sign(); ``` -### Verification code choice +#### Verification code choice This is more secure than previous example as the app forces user to look up the verification code displayed to him and pick the same verification code from 3 different codes displayed in Smart-ID app and thus tries to assure that user is not interacting with some other service. @@ -472,10 +471,10 @@ catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException } ``` -### Long confirmation message with fallback to PIN +#### Long confirmation message with fallback to PIN Relying Party first choice is confirmationMessage that can be up to 200 characters long. -If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interactionDeprecated. +If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interaction. ```java @@ -498,7 +497,7 @@ else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteraction } ``` -### Long confirmation message together with verification code choice with fallback to verification code choice +#### Long confirmation message together with verification code choice with fallback to verification code choice Relying Party first choice is confirmationMessage followed by verification code choice. If this is not available then only verification code choice with shorter text is displayed. @@ -549,7 +548,7 @@ try { .sign(); } catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("User's Smart-ID app is not capable of displaying required interactionDeprecated"); + System.out.println("User's Smart-ID app is not capable of displaying required interaction"); } ``` @@ -689,7 +688,11 @@ you have two alternatives: # How to use API v3.0 Support for Smart-ID API v3.0 has been added to the library. The code for v3.0 is located under the ee.sk.smartid.v3 package. -This version introduces new dynamic link and notification-based flows for both authentication and signing. +This version introduces new dynamic link and notification-based flows for authentication, certificate choice and signing. + +NB! v2 API classes are still available under the ee.sk.smartid.v2 package. +Some classes that were not specific to only v2 have not been moved. Aim was to provide easier way to migrate from v2 to v3. +For example v3 dynamic-link authentication can be still implemented so v2 signing stays the same. This way incremental migration is possible. To use the v3.0 API, import the relevant classes from the ee.sk.smartid.v3 package. ```java @@ -700,20 +703,53 @@ To use the v3.0 API, import the relevant classes from the ee.sk.smartid.v3 packa ## Setting up SmartIdClient for v3.0 ```java - import ee.sk.smartid.v3.SmartIdClient; +import ee.sk.smartid.v3.SmartIdClient; + +InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); var client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - client.setTrustStore(trustStore); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setTrustStore(trustStore); ``` -## Dynamic Link flows +## Dynamic-link flows + +Dynamic-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. +More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/dynamic_link_flows.html + +### Dynamic-link authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V1. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V1 signature protocol. + * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. +* `allowedInteractionsOrder`: Required. An array of objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -### Examples of performing dynamic link authentication +#### Examples of initiating a dynamic-link authentication session -#### Initiating anonymous authentication session +##### Initiating an anonymous authentication session Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. RP can learn the user's identity only after the user has authenticated themselves. @@ -725,13 +761,12 @@ String randomChallenge = RandomChallenge.generate(); // Used for validating authentication sessions status OK response DynamicLinkSessionResponse authenticationSessionResponse = client - .createAuthentication() + .createDynamicLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withAllowedInteractionsOrder(Collections.singletonList( - // before the user can enter PIN. If user selects wrong verification code then the operation will fail. - Interaction.verificationCodeChoice("Log in to self-service?") + DynamicLinkInteraction.displayTextAndPIN("Log in?") )) .initAuthenticationSession(); @@ -739,14 +774,17 @@ String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// Start querying sessions status ``` Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -#### Initiating authentication session with semantics identifier +##### Initiating a dynamic-link authentication session with semantics identifier More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) @@ -767,332 +805,266 @@ DynamicLinkSessionResponse authenticationSessionResponse = client .createDynamicLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) .initAuthenticationSession(); String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +// Start querying sessions status ``` Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -#### Initiating authentication session with document number - -Authentication with document number is mostly for re-authentication. -After the user has authenticated once, the document number is returned in the authentication response. `todo: check if this is correct` +##### Initiating a dynamic-link authentication session with document number ```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication +String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new hash value must be created for each new authentication request String randomChallenge = RandomChallenge.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response +// Used for validating OK authentication sessions status response DynamicLinkSessionResponse authenticationSessionResponse = client .createDynamicLinkAuthentication() .withDocumentNumber(documentNumber) .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) .initAuthenticationSession(); String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later String sessionToken = authenticationSessionResponse.getSessionToken(); -String sessionSecret = authenticationSessionResponse.getSessionSecret(); // Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.getSessionSecret(); +Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +// Start querying sessions status ``` -Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. -Jump to [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Initiating a Dynamic Link Certificate Choice Session +### Dynamic-link certificate choice session !!!Dynamic-link Certificate Choice session Cannot be used at the moment!!! -The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a certificate choice session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the authentication process. +The Smart-ID API v3.0 introduces dynamic-link certificate choice session. This allows more secure way of initiating signing. +Scanning QR-code or clicking on dynamic link will prove that the certificates of the device being used for signing is in the proximity where the signing was initiated. #### Request Parameters -The request parameters for the dynamic link certificate choice session are: - * `relyingPartyUUID`: UUID of the Relying Party. * `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. * `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -#### Example: Initiating a Dynamic Link Certificate Choice Request -Here's an example of how to initiate a dynamic link certificate choice request using the Smart-ID Java client. +#### Response parameters -```java -SmartIdClient client=new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() +#### Examples of initiating a dynamic-link certificate choice session + +##### Initiating an anonymous certificate choice session +```java +DynamicLinkSessionResponse certificateChoice = client.createDynamicLinkCertificateRequest() .withRelyingPartyUUID(client.getRelyingPartyUUID()) .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel("QUALIFIED") - .withNonce("1234567890") - .withShareMdClientIpAddress(true) + .withCertificateLevel(CertificateLevel.QUALIFIED) .initiateCertificateChoice(); -// Note: After a certificate choice request, a notification-based signature choice must follow. -``` - -#### Example of Initiating a dynamic link certificate choice request with `QUALIFIED` certificate level and IP sharing enabled. -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - - DynamicLinkSessionResponse response = client.createDynamicLinkCertificateRequest() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withShareMdClientIpAddress(true) - .initiateCertificateChoice(); -``` - -#### Response on Successful Certificate Choice Session Creation -The response from a successful dynamic link certificate choice session creation contains the following parameters: - -* `sessionID`: A string that can be used to request the operation result. -* `sessionToken`: Unique random value that will be used to connect this certificate choice attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. - -#### Validating Parameters -Ensure that you validate the parameters before initiating the request. For example, the `nonce` must be between 1 and 30 characters. - -#### Error Handling -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `SmartIdClientException`, and others. - -```java -try { - CertificateChoiceResponse response = builder.initCertificateChoice(); - // Proceed with session status fetching and validation -} catch (UserAccountNotFoundException e) { - System.out.println("User account not found."); -} catch (SmartIdClientException e) { - System.out.println("Client exception occurred: " + e.getMessage()); -} -``` +String sessionId = certificateChoice.getSessionID(); +// SessionID is used to query sessions status later -#### `Request Properties`: If you need the IP address of the user's device, set only shareMdClientIpAddress to true. There is no need to create a full RequestProperties object for this. -```java -client.createDynamicLinkCertificateRequest().withShareMdClientIpAddress(true); +String sessionToken = certificateChoice.getSessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = certificateChoice.getSessionSecret(); +Instant responseReceivedAt = certificateChoice.getReceivedAt(); ``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -* `Capabilities`: The capabilities parameter is an optional field used only when an agreement is established with the Smart-ID provider. If this parameter is omitted, the requested capabilities are automatically derived from the `certificateLevel`. Supported certificate levels include: -* `ADVANCED`: A certificate for advanced electronic signatures. -* `QUALIFIED`: A qualified certificate under the eIDAS regulation. -* `QSCD`: A qualified certificate that is also QSCD-capable, marking a higher level of security for qualified signatures. - -### Initiating a Dynamic Link Signature Session -The Smart-ID API v3.0 introduces dynamic link flows, allowing you to initiate a signature session without prior knowledge of the user's identity or device. This is useful for scenarios where the user is not identified yet, and you want to initiate the signing process using a dynamic link. The user can then access the link and complete the signing process. +### Dynamic-link signature session #### Request Parameters -The request parameters for the dynamic link signature session are as follows: + +The request parameters for the dynamic-link signature session are as follows: * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. - * Each interactionDeprecated object includes: - * `type`: Required. Type of interactionDeprecated. Allowed types are `displayTextAndPIN`, `confirmationMessage`. +* `allowedInteractionsOrder`: Required. An array of objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -#### Initiating a Dynamic Link Signature Session Using Semantics Identifier -```java -var client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +#### Response Parameters + +The response from a successful dynamic-link signature session creation contains the following parameters: + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +#### Examples of initiating a dynamic-link signature session + +##### Initiating a dynamic-link signature session with semantics identifier + +```java // Create the signable data var signableData = new SignableData("Test data to sign".getBytes()); signableData.setHashType(HashType.SHA256); // Create the Semantics Identifier var semanticsIdentifier = new SemanticsIdentifier( -SemanticsIdentifier.IdentityType.PNO, -SemanticsIdentifier.CountryCode.EE,"31111111111"); + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); -// Build the dynamic link signature request -var builder = client.createDynamicLinkSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) +// Initiate the dynamic-link signature +DynamicLinkSessionResponse signatureResponse = client.createDynamicLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); - -// Initiate the dynamic link signature -DynamicLinkSessionResponse signatureResponse = builder.initSignatureSession(); + .withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); // Process the signature response String sessionID = signatureResponse.getSessionID(); String sessionToken = signatureResponse.getSessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = signatureResponse.getSessionSecret(); +Instant receivedAt = signatureResponse.getReceivedAt(); -// Use the session information as needed +// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// Start querying sessions status ``` +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -#### Initiating a Dynamic Link Signature Session Using Document Number -```java -SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +##### Initiating a dynamic-link signature session with document number +```java // Create the signable data var signableData = new SignableData("Test data to sign".getBytes()); signableData.setHashType(HashType.SHA256); // Specify the document number -String documentNumber = "PNOEE-31111111111-MOCK-Q"; +String documentNumber = "PNOEE-40504040001-MOCK-Q"; -// Build the dynamic link signature request -var builder = client.createDynamicLinkSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) +// Build the dynamic-link signature request +DynamicLinkSessionResponse signatureResponse = client.createDynamicLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of(Interaction.displayTextAndPIN("Please sign the document"))); - -// Initiate the dynamic link signature -DynamicLinkSessionResponse signatureResponse = builder.initSignatureSession(); + .withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); // Process the signature response String sessionID = signatureResponse.getSessionID(); String sessionToken = signatureResponse.getSessionToken(); + +// Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = signatureResponse.getSessionSecret(); +Instant receivedAt = signatureResponse.getReceivedAt(); -// Use the session information as needed +// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureResponse +// Start querying sessions status ``` -Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. -Jump to [Examples of Allowed Dynamic-link Interactions Order](#examples-of-allowed-dynamic-link-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. - -### Response Parameters -The response from a successful dynamic link signature session creation contains the following parameters: - -* `sessionID`: A string that can be used to request the operation result. -* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Error Handling -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException`, `SessionTimeoutException`, and others. +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException` and others. ```java try { -DynamicLinkSessionResponse response = builder.initSignatureSession(); - -String sessionID = response.getSessionID(); -String sessionToken = response.getSessionToken(); -String sessionSecret = response.getSessionSecret(); - -System.out.println("Session ID: " + sessionID); -System.out.println("Session Token: " + sessionToken); -System.out.println("Session Secret: " + sessionSecret); - + DynamicLinkSessionResponse response = builder.init*Session(); } catch (UserAccountNotFoundException e) { -System.out.println("User account not found."); + System.out.println("User account not found."); } catch (RelyingPartyAccountConfigurationException e) { -System.out.println("Relying party account configuration issue."); + System.out.println("Relying party account configuration issue."); } catch (RequiredInteractionNotSupportedByAppException e) { -System.out.println("The required interactionDeprecated is not supported by the user's app."); + System.out.println("The required interaction is not supported by the user's app."); } catch (ServerMaintenanceException e) { -System.out.println("Server maintenance in progress, please try again later."); + System.out.println("Server maintenance in progress, please try again later."); } catch (SmartIdClientException e) { -System.out.println("An error occurred: " + e.getMessage()); + System.out.println("An error occurred: " + e.getMessage()); } ``` -### Additional Information -* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. Examples include `displayTextAndPIN`, `confirmationMessage`. - -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessage("Please confirm the transaction of 1024.50 EUR"), - Interaction.displayTextAndPIN("Confirm transaction") -)); -``` - -* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. - -```java -var parameters = new RawDigestSignatureProtocolParameters(); -parameters.setDigest(signableData.calculateHashInBase64()); -parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); -builder.withSignatureProtocolParameters(parameters); -``` +### Additional dynamic-link session request properties -* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. +#### Using nonce to override idempotent behaviour +Authentication is used as an example, nonce can also be used with certificate choice and signature sessions requests by using method `withNonce("randomValue")`. ```java -var requestProperties = new RequestProperties(); -requestProperties.setShareMdClientIpAddress(true); -builder.withRequestProperties(requestProperties); +DynamicLinkSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) + // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned + // set nonce to override idempotent behaviour + .withNonce("randomValue") + .initAuthenticationSession(); ``` +#### Using request properties to request the IP address of the user's device -* `Nonce`: A unique identifier (up to 30 characters) used to manage idempotent behavior in session creation requests. If a request is repeated within a 15-second timeframe, the same session ID may be returned unless a different nonce is provided. - -```java -builder.withNonce("randomNonce123"); -``` +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing -* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. ```java -builder.withCapabilities(Set.of("QUILIFIED", "ADVANCED")); +DynamicLinkSessionResponse authenticationSessionResponse = client + .createDynamicLinkAuthentication() + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); ``` -* `Certificate Level`: Set the required certificate level (`ADVANCED` or `QUALIFIED`). Defaults to `QUALIFIED`. +### Examples of allowed dynamic-link interactions order -```java -builder.withCertificateLevel(CertificateLevel.QUALIFIED); -``` - -### Examples of Allowed Dynamic-link Interactions Order -An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. For dynamic link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. -Each interaction is defined by an object that includes a type and either displayText60 (for shorter texts) or displayText200 (for longer texts). +DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons +and a second screen to enter the PIN-code. Below are examples of allowedInteractionsOrder elements specifically for dynamic link flows: @@ -1100,13 +1072,13 @@ Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. ```java builder.withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), - DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") - )); + DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), + DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") +)) ``` Example 2: `displayTextAndPIN` Only -Description: Use `displayTextAndPIN` interactionDeprecated only. +Description: Use `displayTextAndPIN` interaction only. ```java builder.withAllowedInteractionsOrder(List.of( DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") @@ -1123,53 +1095,55 @@ builder.withAllowedInteractionsOrder(List.of( ### Generating QR-code or dynamic link +Documentation to dynamic link and QR-code requirements +https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/dynamic_link_flows.html#_dynamic_link_and_qr_presentation + #### Generating dynamic link Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. -Providing QR-code as a dynamic link type will allow generating QR-code at frontend side. -#### Dynamic link parameters +##### Dynamic link parameters * `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. * `version`: Version of the dynamic link. Default value is `0.1`. * `dynamicLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. * `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. * `sessionToken`: Token from the session response. -* `elapsedTime`: Elapsed time from when the session response was received. +* `elapsedSeconds`: Elapsed time from when the session response was received. * `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link -* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType and current time and session secret. Session secret can be found in the session response. +* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType, calculated elapsed seconds since response was received and session secret. Received at and sessions secret can be found from the session response. ```java -DynamicLinkSessionResponse response; // response from the session initiation query. -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); +DynamicLinkSessionResponse sessionResponse; // response from the session initiation query. +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(sessionResponse.getReceivedAt(), Instant.now()).getSeconds(); // Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionResponse.getSessionSecret()); // Generate dynamic link URI dynamicLink = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time .withAuthCode(authCode) .createUri(); ``` -#### Overriding default values +##### Overriding default values ```java DynamicLinkSessionResponse response; // response from the session initiation query. -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); // Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); // Generate dynamic link URI dynamicLink = client.createDynamicContent() .withBaseUrl("https://example.com") // override default base URL (https://smart-id.com/dynamic-link) - .withDynamicLinkType(DynamicLinkType.QR_CODE) // specify the type of dynamic link + .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withElapsedSeconds(elapsedSeconds) .withUserLanguage("est") // override default user language (eng) .withAuthCode(authCode) .createUri(); @@ -1179,29 +1153,30 @@ URI dynamicLink = client.createDynamicContent() Creating a QR code uses the Zxing library to generate a QR code image with dynamic link as content. According to link size the QR-code of version 9 (53x53 modules) is used. -For the QR-code to be scannable by most devices the QR code module size should be 10px. +For the QR-code to be scannable by most devices the QR code module size should be ~10px. It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). Generated QR code will have error correction level low. -#### Generate QR-code Data URI +##### Generate QR-code Data URI ```java -DynamicLinkSessionResponse response; // init auth sessions response -// Capture and store when initiating sessions response arrived -Instant responseReceivedTime = Instant.now(); +DynamicLinkSessionResponse response; // response from the session initiation query. + +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); // Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) String qrCodeDataUri = client.createDynamicContent() .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withElapsedSeconds(elapsedSeconds) .withAuthCode(authCode) - .createQrCode(); + .createQrCodeDataUri(); ``` -#### Generate QR-code with custom height, width, quiet area and image format +##### Generate QR-code with custom height, width, quiet area and image format Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. QR code version 9 (53x53 modules) is automatically selected by content size @@ -1210,30 +1185,34 @@ Other image size in range 366px to 1159px is also possible. Width and height of The width and height of 1159px produce a QR code with a module size of 19px. ```java -DynamicLinkSessionResponse response; // init auth sessions response -// Capture and store when initiating session response arrived -Instant responseReceivedTime = Instant.now(); +DynamicLinkSessionResponse response; // response from the session initiation query. + +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); // Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, response.getSessionSecret()); +String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); // Generate dynamic link -URI qrDataUri = client.createDynamicContent() +URI qrDynamicLink = client.createDynamicContent() .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for, possible values (auth, sign, cert) .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(Duration.between(responseReceivedTime, Instant.now())) // calculate elapsed seconds from response received time + .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time .withAuthCode(authCode) .createUri(); +// At this point URI can be returned to frontend and QR-code could be generated from it at frontend side. Or continue to next steps. -// Generate QR-code with height and width of 570px and quiet area of 2 modules. +// Create QR-code with height and width of 570px and quiet area of 2 modules. BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(qrDataUri, 570, 570, 2); // Convert BufferedImage to Data URI String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); +// Return Data URI to frontend and display the QR-code ``` ## Session status request handling for v3.0 -The Smart-ID v3.0 API includes new session status request paths for retrieving session results. +The Smart-ID v3.0 API includes new session status request path for retrieving session results. +Session status request is to be used for dynamic-link and notification-based flows. ### Session status response @@ -1242,66 +1221,54 @@ The session status response includes various fields depending on whether the ses * `state`: RUNNING or COMPLETE * `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) * `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. -* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature sessions) +* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) * `signature`: Contains the following fields based on the signatureProtocol used: * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm * `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). * `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionFlowUsed`: The interactionDeprecated flow used for the session. +* `interactionFlowUsed`: The interaction flow used for the session. * `deviceIpAddress`: IP address of the mobile device, if requested. -### Example of fetching session status in v3.0 +### Examples of querying session status in v3.0 #### Example of using session status poller to query final sessions status + The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. ```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Client setup with TrustStore. Requests will not work without a valid certificate. -InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); -client.setTrustStore(trustStore); - -// +*SessionResponse sessionResponse; +// Get the session status poller SessionsStatusPoller poller = client.getSessionsStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); -if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); +// Get sessionID from current session response +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID()); + +// Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) +if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + System.out.println("Session completed with result: "+sessionStatus.getResult().getEndResult()); } ``` -#### Example of querying sessions status -The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. +#### Example of querying sessions status only once +The following example shows how to use the SessionStatusPoller to only query the sessions status single time. +NB! If using this method for dynamic-link flows. Make sure the pollingSleepTimeout and ```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Client setup with TrustStore. Requests will not work without a valid certificate. -InputStream is = SmartIdClient.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); -client.setTrustStore(trustStore); - +*SessionResponse sessionResponse; // Get the session status poller -SessionsStatusPoller poller = client.getSessionsStatusPoller(); +SessionStatusPoller poller = client.getSessionStatusPoller(); -// Queryinn -SessionStatus sessionStatus = poller.getSessionsStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); -if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { +// Querying the sessions status +SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.getSessionID()); +// Checking sessions state +if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { // Session is still running and querying can be continued // Dynamic content can be generated and displayed to the user +} else if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + // continue to validate the sessions status } else { - // continue to the next step + throw UnprocessableSmartIdResponseException("Invalid session state was returned"); } ``` @@ -1317,43 +1284,68 @@ It's important to validate the session status response to ensure that the return #### Example of validating the authentication sessions response: ```java +DyanmicLinkSessionResponse sessionResponse; +// get sessions result +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID(), 10000); + +// validate sessions state is completed +if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + // validate sessions status result and map session status to authentication response + AuthenticationResponse response = AuthenticationResponseMapper.from(sessionStatus); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity, uses certificate level QUALIFIED as default. + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.toAuthenticationIdentity(response, "randomChallenge"); +} +``` + +##### Authentication response validator setup + +````java // init authentication response validator with trusted certificates // there are 4 different ways to initialize the validator // 1. use default values `trusted_certificates.jks` with password `changeit` AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); + // 2. provide your own path to truststore and truststore password AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(truststorePath, truststorePassword); -// 3 read trusted certificate yourself and provide it to the validator + +// 3. read trusted certificate yourself and provide it to the validator X509Certificate[] trustedCertificates = findTrustedCertificates(); AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCertificates); + // 4. init authentication response validator with the empty array and add trusted certificates AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[0]); X509Certificate certificate = getTrustedCertificate(); authenticationResponseValidator.addTrustedCACertificate(certificate); +```` -// get sessions result -SessionStatus sessionStatus = poller.fetchFinalSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016", 10000); +#### Example of validating the certificate choice session response: -// validate sessions state is completed -if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - // validate sessions status result and map session status to authentication response - DynamicLinkAuthenticationResponse response = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step - - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.from(response, "randomChallenge"); +```java +try { + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(sessionStatus); +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); } ``` -#### Example of validating the signature: +#### Example of validating the signature session response: ```java try { - // Map and validate the session status + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. SignatureResponse signatureResponse = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); // Process the response (e.g., save to database or pass to another system) - handleSignatureResponse(signatureResponse); + handleSignatureResponse(signatureResponse); } catch (UserRefusedException e) { System.out.println("User refused the session."); @@ -1375,7 +1367,7 @@ The session status response may return various error codes indicating the outcom * `TIMEOUT`: User did not respond in time. * `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. * `WRONG_VC`: User selected the wrong verification code. -* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interactionDeprecated is not supported by the user's app. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. * `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. * `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). * `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. @@ -1384,23 +1376,55 @@ The session status response may return various error codes indicating the outcom ## Notification-based flows -### Differences Between Notification-Based and Dynamic Link Flows +### Differences between notification-based and dynamic-link flows + * `Notification-Based flow` * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. - * Known users or devices: Suitable when the RP already knows the user's identity or device. + * Known users or devices: + * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using dynamic-link flows. * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. * `Dynamic Link flow` * Dynamic links: Generates links like QR codes or Web2App/App2App links that the user interacts with to start the process. * Supports unknown users or devices: Useful when the user's identity or device is not known in advance. - * Real-time updates: Dynamic links need to be refreshed every second to ensure validity, especially for QR codes. + * Real-time updates: Dynamic links and QR-code need to be refreshed every second to ensure validity. + +### Notification-based authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V1. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V1 signature protocol. + * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. +* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response parameters +* `sessionID`: Required. String used to request the operation result. +* `verificationCode`: Required. Object describing the Verification Code to be displayed. + * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. + * `value`: Required. Value of the VC code. + +#### Examples of initiating a notification-based authentication session -### Examples of performing notification authentication +##### Initiating a notification-based authentication session with document number -#### Initiating notification authentication session with document number ```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; +String documentNumber = "PNOLT-40504040001-MOCK-Q"; +// For security reasons a new hash value must be created for each new authentication request String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() @@ -1408,7 +1432,7 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("Log in to self-service?") + NotificationInteraction.verificationCodeChoice("Log in?") )) .initAuthenticationSession(); @@ -1418,18 +1442,21 @@ String sessionId = authenticationSessionResponse.getSessionID(); String verificationCode = authenticationSessionResponse.getVc().getValue(); // Display the verification code to the user for confirmation ``` -After initiating the session, display the verificationCode to the user. The user must confirm that the code displayed in their Smart-ID app matches the one you have provided. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based authentication session with semantics identifier -#### Initiating notification authentication session with semantics identifier -Alternatively, you can initiate a notification authentication session using a semantics identifier, which uniquely identifies the user across different countries and identity types. ```java SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, - "30303039914" + "40504040001" ); +// For security reasons a new hash value must be created for each new authentication request String randomChallenge = RandomChallenge.generate(); +// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() @@ -1437,7 +1464,7 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("Log in to self-service?") + NotificationInteraction.verificationCodeChoice("Log in?") )) .initAuthenticationSession(); @@ -1447,13 +1474,11 @@ String sessionId = authenticationSessionResponse.getSessionID(); String verificationCode = authenticationSessionResponse.getVc().getValue(); // Display the verification code to the user for confirmation ``` -Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. -Jump to [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Initiating a Notification Certificate Choice Session +### Notification-based certificate choice session -#### Request Parameters -The request parameters for the dynamic link certificate choice session are: +#### Request parameters * `relyingPartyUUID`: UUID of the Relying Party. * `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. @@ -1462,44 +1487,50 @@ The request parameters for the dynamic link certificate choice session are: * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. * `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -#### Initiating a Notification Certificate Choice Using Semantics Identifier +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. + +#### Examples of initiating a notification-based certificate choice session + +##### Initiating a notification-based certificate choice session using semantics identifier + ```java SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( // 3 character identity type // (PAS-passport, IDC-national identity card or PNO - (national) personal number) SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "30303039914"); // identifier (according to country and identity type reference) + "40504040001"); // identifier (according to country and identity type reference) NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client .createNotificationCertificateChoice() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .withNonce("1234567890") + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = certificateChoiceSessionResponse.getSessionID(); // SessionID is used to query sessions status later ``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based certificate choice session using document number -#### Initiating a Notification Certificate Choice Using Document Number ```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; // returned in authentication result and used for re-authentication +String documentNumber = "PNOLT-30303039914-MOCK-Q"; - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .withNonce("1234567890") - .initCertificateChoice(); +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = certificateChoiceSessionResponse.getSessionID(); // SessionID is used to query sessions status later ``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Initiating a Notification-Based Signature Session - -The Smart-ID API v3.0 allows you to initiate a signature session using a notification-based flow. This method is useful when the user is already known or authenticated, and you want to initiate the signing process directly through a notification to the user's device, without the need for a dynamic link. +### Notification-based signature session #### Request Parameters The request parameters for the notification-based signature session are as follows: @@ -1508,28 +1539,29 @@ The request parameters for the notification-based signature session are as follo * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `rawDigestSignatureProtocolParameters`: Required for RAW_DIGEST_SIGNATURE. Parameters for the signature protocol. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of interactionDeprecated objects defining the allowed interactions in order of preference. - * Each interactionDeprecated object includes: - * `type`: Required. Type of interactionDeprecated. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. +* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -#### Example: Initiating a Notification-Based Signature Request -Below is an example of how to initiate a notification-based signature request using the Smart-ID Java client, using both the Semantics Identifier and Document Number endpoints. +#### Response Parameters +* `sessionID`: Required. String used to request the operation result. +* `verificationCode`: Required. Object describing the Verification Code to be displayed. + * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. + * `value`: Required. Value of the VC code. -#### Initiating a Notification-Based Signature Session Using Semantics Identifier -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("your-relying-party-uuid"); -client.setRelyingPartyName("your-relying-party-name"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +#### Examples of initiating a notification-based signature session +##### Initiating a notification-based signature session with semantics identifier + +```java // Create the signable data SignableData signableData = new SignableData("Data to sign".getBytes()); signableData.setHashType(HashType.SHA256); @@ -1538,81 +1570,59 @@ signableData.setHashType(HashType.SHA256); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, - "31111111111" + "40504040001" ); // Build the notification signature request -NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() +NotificationSignatureSessionResponse signatureSessionResponse = client.createNotificationSignature() .withRelyingPartyUUID(client.getRelyingPartyUUID()) .withRelyingPartyName(client.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) .withAllowedInteractionsOrder(List.of( - Interaction.verificationCodeChoice("Please sign the document") - )); + NotificationInteraction.verificationCodeChoice("Please sign the document"))) + .initSignatureSession(); -// Initiate the notification signature session -NotificationSignatureSessionResponse signatureResponse = builder.initSignatureSession(); +// Process the querying sessions status response +String sessionID = signatureSessionResponse.getSessionID(); -// Process the signature response -String sessionID = signatureResponse.getSessionID(); -Vc verificationCode = signatureResponse.getVc(); - -System.out.println("Session ID: " + sessionID); -System.out.println("Verification Code Type: " + verificationCode.getType()); -System.out.println("Verification Code Value: " + verificationCode.getValue()); +// Display verification code to the user +String verificationCode = signatureSessionResponse.getVc().getValue(); ``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -#### Initiating a Notification-Based Signature Session Using Document Number -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("your-relying-party-uuid"); -client.setRelyingPartyName("your-relying-party-name"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +##### Initiating a notification-based signature session with document number +```java // Create the signable data SignableData signableData = new SignableData("Data to sign".getBytes()); signableData.setHashType(HashType.SHA256); // Specify the document number -String documentNumber = "PNOEE-31111111111-MOCK-Q"; +String documentNumber = "PNOEE-40504040001-MOCK-Q"; -// Build the notification signature request -NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() +// Initiate the session +NotificationSignatureSessionResponse signatureResponse = client.createNotificationSignature() .withRelyingPartyUUID(client.getRelyingPartyUUID()) .withRelyingPartyName(client.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withDocumentNumber(documentNumber) .withAllowedInteractionsOrder(List.of( - Interaction.verificationCodeChoice("Please sign the document") - )); - -// Initiate the notification signature session -NotificationSignatureSessionResponse signatureResponse = builder.initSignatureSession(); + NotificationInteraction.verificationCodeChoice("Please sign the document"))) + .initSignatureSession(); // Process the signature response String sessionID = signatureResponse.getSessionID(); -Vc verificationCode = signatureResponse.getVc(); -System.out.println("Session ID: " + sessionID); -System.out.println("Verification Code Type: " + verificationCode.getType()); -System.out.println("Verification Code Value: " + verificationCode.getValue()); +// Display verification code to the user +String verificationCode = signatureResponse.getVc().getValue(); ``` -Jump to [Requesting the IP Address of the User's Device](#requesting-the-ip-address-of-the-users-device) to see how to request the IP address of the user's device. -Jump to [Examples of Allowed Notification-based Interactions Order](#examples-of-allowed-notification-based-interactions-order) to see how to set the order of preferred interactions for displaying text and asking PIN. - -#### Response on Successful Notification-based Signature Session Creation -Upon successful initiation, the user will receive a notification on their Smart-ID app to complete the signing process. The response includes the `sessionID` and a `verificationCode` (Verification Code) object. - -### Response Parameters -* `sessionID`: Required. String used to request the operation result. -* `verificationCode`: Required. Object describing the Verification Code to be displayed. - * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. - * `value`: Required. Value of the VC code. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Error Handling + Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: * `UserAccountNotFoundException` * `RelyingPartyAccountConfigurationException` @@ -1621,24 +1631,16 @@ Handle exceptions appropriately. The Java client provides specific exceptions fo * `ServerMaintenanceException` * `SmartIdClientException` -### Example of Error Handling +#### Example of Error Handling ```java try { NotificationSignatureSessionResponse response = builder.initSignatureSession(); - - String sessionID = response.getSessionID(); - Vc verificationCode = response.getVc(); - - System.out.println("Session ID: " + sessionID); - System.out.println("Verification Code Type: " + verificationCode.getType()); - System.out.println("Verification Code Value: " + verificationCode.getValue()); - } catch (UserAccountNotFoundException e) { System.out.println("User account not found."); } catch (RelyingPartyAccountConfigurationException e) { System.out.println("Relying party account configuration issue."); } catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interactionDeprecated is not supported by the user's app."); + System.out.println("The required interaction is not supported by the user's app."); } catch (ServerMaintenanceException e) { System.out.println("Server maintenance in progress, please try again later."); } catch (SmartIdClientException e) { @@ -1646,113 +1648,70 @@ try { } ``` -### Additional Information -* `Allowed Interactions Order`: Define the preferred interactions for displaying text and asking for PIN. The app will pick the first interactionDeprecated it supports from the list. For notification-based flows, use `verificationCodeChoice` and `confirmationMessageAndVerificationCodeChoice`. -```java -builder.withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm the transaction of 1024.50 EUR"), - Interaction.verificationCodeChoice("Confirm transaction") -)); -``` +### Additional notification-based session request parameters -* `Signature Protocol Parameters`: Specify the signature protocol parameters as required for `RAW_DIGEST_SIGNATURE`. -```java -var parameters = new RawDigestSignatureProtocolParameters(); -parameters.setDigest(signableData.calculateHashInBase64()); -parameters.setSignatureAlgorithm("sha512WithRSAEncryption"); -builder.withSignatureProtocolParameters(parameters); -``` +#### Using nonce to override idempotent behaviour -* `Request Properties`: Include additional properties in the request, such as requesting the IP address of the user's device. -```java -var requestProperties = new RequestProperties(); -requestProperties.setShareMdClientIpAddress(true); -builder.withRequestProperties(requestProperties); -``` +Authentication is used as an example, nonce can also be used with certificate choice and signature sessions requests by using method `withNonce("randomValue")`. -* `Nonce`: A random string up to 30 characters to associate the request with a specific session or transaction. ```java -builder.withNonce("randomNonce123"); -``` - -* `Capabilities`: Specify capabilities if agreed with the Smart-ID provider. When omitted, capabilities are derived from the `certificateLevel`. -```java -builder.withCapabilities(Set.of("QUALIFIED", "ADVANCED")); -``` - -* `Certificate Level`: Set the required certificate level (`ADVANCED`, `QUALIFIED`, or `QSCD`). Defaults to `QUALIFIED`. -```java -builder.withCertificateLevel(CertificateLevel.QUALIFIED); +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + NotificationInteraction.verificationCodeChoice("Log in?") + )) + // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned + // set nonce to override idempotent behaviour + .withNonce("randomValue") + .initAuthenticationSession(); ``` -### Full Example -Here's a complete example of initiating a notification-based signature session: -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("your-relying-party-uuid"); -client.setRelyingPartyName("your-relying-party-name"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - -// Prepare the signable data -SignableData signableData = new SignableData("Data to sign".getBytes()); -signableData.setHashType(HashType.SHA512); +#### Using request properties to request the IP address of the user's device -// Specify the Semantics Identifier or Document Number -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "31111111111" -); -// Or use document number -// String documentNumber = "PNOEE-31111111111-MOCK-Q"; +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing -// Build the notification signature request -NotificationSignatureSessionRequestBuilder builder = client.createNotificationSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) // or .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - Interaction.confirmationMessageAndVerificationCodeChoice("Please confirm the transaction of 1024.50 EUR"), - Interaction.verificationCodeChoice("Confirm transaction") - )) - .withNonce("randomNonce123") - .withShareMdClientIpAddress(true); +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. -// Initiate the notification signature session -NotificationSignatureSessionResponse response = builder.initSignatureSession(); - -// Process the response -String sessionID = response.getSessionID(); -Vc verificationCode = response.getVc(); +```java +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + NotificationInteraction.verificationCodeChoice("Log in?") + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); +``` -System.out.println("Session ID: " + sessionID); -System.out.println("Verification Code Type: " + verificationCode.getType()); -System.out.println("Verification Code Value: " + verificationCode.getValue()); +### Examples of allowed notification-based interactions order -// Proceed with session status polling to obtain the signature -SessionStatusPoller poller = client.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionID); +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. +Different interactions can support different amounts of data to display information to the user. -// Extract the signature from the session status -SmartIdSignature signature = SmartIdSignature.fromSessionStatus(sessionStatus); +Below are examples of `allowedInteractionsOrder`. -// Use the signature as needed +Example 1: `confirmationMessageAndVerificationCodeChoice` with Fallback to `verificationCodeChoice` +Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; if not available, then fall back to `verificationCodeChoice`. +```java +builder.withAllowedInteractionsOrder(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), + NotificationInteraction.verificationCodeChoice("Up to 60 characters of text here...") +)); ``` -### Examples of Allowed Notification-based Interactions Order -An app can support different interactionDeprecated types, and a Relying Party can specify the preferred interactions with or without fallback options. -Different interactions can support different amounts of data to display information to the user. - -Below are examples of `allowedInteractionsOrder` elements in JSON format: - Example 1: `verificationCodeChoice` only Description: Use `verificationCodeChoice` interaction exclusively. ```java builder.withAllowedInteractionsOrder(List.of( NotificationInteraction.verificationCodeChoice("Up to 60 characters of text here...") - )); +)); ``` Example 2: `confirmationMessageAndVerificationCodeChoice` only @@ -1760,22 +1719,7 @@ Description: Insist on `confirmationMessageAndVerificationCodeChoice`; if not av ```java builder.withAllowedInteractionsOrder(List.of( NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") - )); -``` - -## Requesting the IP Address of the User's Device -If you need to retrieve the user's device IP address as part of the authentication session, you can include the `withShareMdClientIpAddress(true)` method in the request. Note that this feature must be enabled by the Smart-ID service provider. -```java -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .withShareMdClientIpAddress(true) // Request the user's device IP address - .initAuthenticationSession(); +)); ``` ## Exception Handling diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java index 1af44d94..2a1b73b5 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,4 +34,8 @@ public class CertificateLevelMismatchException extends UserAccountException { public CertificateLevelMismatchException() { super("Signer's certificate is below requested certificate level"); } + + public CertificateLevelMismatchException(String message) { + super(message); + } } diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index ce0aa48f..7efbd174 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index f1f77da8..e150b0b2 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index bc8e230f..fe3991c8 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java index de2fa49a..69090e2b 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java index 382ee85e..eb9dca76 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java index 281d87ba..c1ca6a9e 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/CertificateLevel.java b/src/main/java/ee/sk/smartid/v2/CertificateLevel.java index c2804b92..6478c2ad 100644 --- a/src/main/java/ee/sk/smartid/v2/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateLevel.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java index a0729df1..7505a4bb 100644 --- a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SignableData.java b/src/main/java/ee/sk/smartid/v2/SignableData.java index f2d6ba02..02e636de 100644 --- a/src/main/java/ee/sk/smartid/v2/SignableData.java +++ b/src/main/java/ee/sk/smartid/v2/SignableData.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SignableHash.java b/src/main/java/ee/sk/smartid/v2/SignableHash.java index 583f6fdd..2b6572c5 100644 --- a/src/main/java/ee/sk/smartid/v2/SignableHash.java +++ b/src/main/java/ee/sk/smartid/v2/SignableHash.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java index 55f3981e..3a100b5a 100644 --- a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java index a23d4c5c..0c1c8601 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java b/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java index a6f5654c..b719b96d 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdClient.java b/src/main/java/ee/sk/smartid/v2/SmartIdClient.java index 5b1d4446..bdd726e5 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdClient.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java index 263f9641..84f1b68b 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java b/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java index c903c9b8..15ed7721 100644 --- a/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java +++ b/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java index ea637b6b..fcaba2b6 100644 --- a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java index 29b4833c..1156f6ab 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java index 77e577a3..cc23742e 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java index fb97bd90..3246be2f 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java index dd843a89..8e336879 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java index e344ad34..3dfab7a5 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java index a733200b..c141610f 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java index 2d68ef87..13b3b785 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java index 21db6785..9d5a19a7 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java index 9fe41e0d..c5a0fde1 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java index 48dfec1c..156f98ac 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java index c2353480..d771e2a3 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java index ed889c0f..cd64983e 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java index 29e5351f..6c4e1faa 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java index b96a024f..ec31ea7d 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java index c00b7418..b0518f91 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java index fdd93987..9084eed8 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java index 69324578..473da6ab 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java index e1209a21..2dc3a198 100644 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java rename to src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java index 685f98a2..a1108e4f 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -38,7 +38,7 @@ * *

      Use with {@link AuthenticationResponseValidator} to validate the certificate and the signature. */ -public class DynamicLinkAuthenticationResponse { +public class AuthenticationResponse { private String endResult; private String serverRandom; diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java similarity index 80% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java rename to src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java index 7737a505..f38eb34e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -41,36 +41,36 @@ import ee.sk.smartid.v3.rest.dao.SessionStatus; /** - * Validates and maps the session status received as dynamic-link authentication response + * Validates and maps the received session status to authentication response */ -public class DynamicLinkAuthenticationResponseMapper { +public class AuthenticationResponseMapper { - private static final Logger logger = LoggerFactory.getLogger(DynamicLinkAuthenticationResponseMapper.class); + private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapper.class); /** - * Maps session status to dynamic-link authentication response + * Maps session status to authentication response * * @param sessionStatus session status received from Smart-ID server - * @return dynamic-link authentication response + * @return authentication response */ - public static DynamicLinkAuthenticationResponse from(SessionStatus sessionStatus) { + public static AuthenticationResponse from(SessionStatus sessionStatus) { validateSessionStatus(sessionStatus); SessionResult sessionResult = sessionStatus.getResult(); SessionSignature sessionSignature = sessionStatus.getSignature(); SessionCertificate sessionCertificate = sessionStatus.getCert(); - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); - dynamicLinkAuthenticationResponse.setEndResult(sessionResult.getEndResult()); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - dynamicLinkAuthenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - dynamicLinkAuthenticationResponse.setCertificate(toCertificate(sessionCertificate)); - dynamicLinkAuthenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); - dynamicLinkAuthenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - dynamicLinkAuthenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - dynamicLinkAuthenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - dynamicLinkAuthenticationResponse.setServerRandom(sessionSignature.getServerRandom()); - return dynamicLinkAuthenticationResponse; + var authenticationResponse = new AuthenticationResponse(); + authenticationResponse.setEndResult(sessionResult.getEndResult()); + authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + authenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + authenticationResponse.setCertificate(toCertificate(sessionCertificate)); + authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); + authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + authenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); + return authenticationResponse; } private static void validateSessionStatus(SessionStatus sessionStatus) { diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java index 772533d5..aa3b13a1 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -69,7 +69,7 @@ public class AuthenticationResponseValidator { * Uses default values to initialize the keystore. */ public AuthenticationResponseValidator() { - initializeTrustedCACertificatesFromKeyStore("trusted_certificates.jks", "changeIt"); + initializeTrustedCACertificatesFromKeyStore("/trusted_certificates.jks", "changeit"); } /** @@ -101,36 +101,36 @@ public void addTrustedCACertificate(X509Certificate certificate) { } /** - * Maps the Smart-ID authentication response {@link DynamicLinkAuthenticationResponse} to {@link AuthenticationIdentity} + * Maps the Smart-ID authentication response {@link AuthenticationResponse} to {@link AuthenticationIdentity} *

      * Uses {@link AuthenticationCertificateLevel#QUALIFIED} as the request certificate level * - * @param dynamicLinkAuthenticationResponse Smart-ID authentication response + * @param authenticationResponse Smart-ID authentication response * @return authentication identity */ - public AuthenticationIdentity toAuthenticationIdentity(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { - return toAuthenticationIdentity(dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, randomChallenge); + public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, String randomChallenge) { + return toAuthenticationIdentity(authenticationResponse, AuthenticationCertificateLevel.QUALIFIED, randomChallenge); } /** - * Maps the Smart-ID authentication response {@link DynamicLinkAuthenticationResponse} to {@link AuthenticationIdentity} + * Maps the Smart-ID authentication response {@link AuthenticationResponse} to {@link AuthenticationIdentity} * - * @param dynamicLinkAuthenticationResponse Smart-ID authentication response - * @param requestedCertificateLevel Certificate level used in the authentication session request - * @param randomChallenge Generate string used in the authentication session request + * @param authenticationResponse Smart-ID authentication response + * @param requestedCertificateLevel Certificate level used in the authentication session request + * @param randomChallenge Generate string used in the authentication session request * @return authentication identity */ - public AuthenticationIdentity toAuthenticationIdentity(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, + public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel, String randomChallenge) { - validateInputs(dynamicLinkAuthenticationResponse, randomChallenge); - validateCertificate(dynamicLinkAuthenticationResponse, requestedCertificateLevel); - validateSignature(dynamicLinkAuthenticationResponse, randomChallenge); - return AuthenticationIdentityMapper.from(dynamicLinkAuthenticationResponse.getCertificate()); + validateInputs(authenticationResponse, randomChallenge); + validateCertificate(authenticationResponse, requestedCertificateLevel); + validateSignature(authenticationResponse, randomChallenge); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); } - private void validateInputs(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { - if (dynamicLinkAuthenticationResponse == null) { + private void validateInputs(AuthenticationResponse authenticationResponse, String randomChallenge) { + if (authenticationResponse == null) { throw new SmartIdClientException("Dynamic link authentication response is not provided"); } if (StringUtil.isEmpty(randomChallenge)) { @@ -138,28 +138,28 @@ private void validateInputs(DynamicLinkAuthenticationResponse dynamicLinkAuthent } } - private void validateCertificate(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (dynamicLinkAuthenticationResponse.getCertificate() == null) { + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (authenticationResponse.getCertificate() == null) { throw new SmartIdClientException("Certificate is not provided"); } - validateCertificateNotExpired(dynamicLinkAuthenticationResponse.getCertificate()); - validateCertificateIsTrusted(dynamicLinkAuthenticationResponse.getCertificate()); - validateCertificateLevel(dynamicLinkAuthenticationResponse, requestedCertificateLevel); + validateCertificateNotExpired(authenticationResponse.getCertificate()); + validateCertificateIsTrusted(authenticationResponse.getCertificate()); + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); } - private void validateSignature(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { - if (StringUtil.isEmpty(dynamicLinkAuthenticationResponse.getAlgorithmName())) { + private void validateSignature(AuthenticationResponse authenticationResponse, String randomChallenge) { + if (StringUtil.isEmpty(authenticationResponse.getAlgorithmName())) { throw new SmartIdClientException("Algorithm name is not provided"); } - if (StringUtil.isEmpty(dynamicLinkAuthenticationResponse.getSignatureValueInBase64())) { + if (StringUtil.isEmpty(authenticationResponse.getSignatureValueInBase64())) { throw new SmartIdClientException("Signature value is not provided"); } try { - Signature signature = getSignature(dynamicLinkAuthenticationResponse); - signature.initVerify(dynamicLinkAuthenticationResponse.getCertificate().getPublicKey()); - String data = createSignatureData(dynamicLinkAuthenticationResponse, randomChallenge); + Signature signature = getSignature(authenticationResponse); + signature.initVerify(authenticationResponse.getCertificate().getPublicKey()); + String data = createSignatureData(authenticationResponse, randomChallenge); signature.update(data.getBytes(StandardCharsets.UTF_8)); - byte[] signedHash = dynamicLinkAuthenticationResponse.getSignatureValue(); + byte[] signedHash = authenticationResponse.getSignatureValue(); if (!signature.verify(signedHash)) { throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); } @@ -169,8 +169,8 @@ private void validateSignature(DynamicLinkAuthenticationResponse dynamicLinkAuth } } - private static Signature getSignature(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse) throws NoSuchAlgorithmException { - String algorithm = dynamicLinkAuthenticationResponse.getAlgorithmName().replace("Encryption", ""); + private static Signature getSignature(AuthenticationResponse authenticationResponse) throws NoSuchAlgorithmException { + String algorithm = authenticationResponse.getAlgorithmName().replace("Encryption", ""); try { return Signature.getInstance(algorithm); } catch (NoSuchAlgorithmException ex) { @@ -179,14 +179,14 @@ private static Signature getSignature(DynamicLinkAuthenticationResponse dynamicL } } - private void validateCertificateLevel(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { if (requestedCertificateLevel == null) { return; } - if (dynamicLinkAuthenticationResponse.getCertificateLevel() == null) { + if (authenticationResponse.getCertificateLevel() == null) { throw new SmartIdClientException("Certificate level is not provided"); } - if (!dynamicLinkAuthenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { throw new CertificateLevelMismatchException(); } } @@ -232,9 +232,9 @@ private static void validateCertificateNotExpired(X509Certificate certificate) { } } - private static String createSignatureData(DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse, String randomChallenge) { + private static String createSignatureData(AuthenticationResponse authenticationResponse, String randomChallenge) { return String.format("%s;%s;%s", SignatureProtocol.ACSP_V1.name(), - dynamicLinkAuthenticationResponse.getServerRandom(), + authenticationResponse.getServerRandom(), randomChallenge); } } diff --git a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java new file mode 100644 index 00000000..e35a1dff --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java @@ -0,0 +1,90 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +/** + * Represents the certificate choice response after a successful certificate choice sessions status response was received. + */ +public class CertificateChoiceResponse { + + private String endResult; + private X509Certificate certificate; + private CertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; + private String deviceIpAddress; + + public String getEndResult() { + return endResult; + } + + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + public CertificateLevel getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + public String getDocumentNumber() { + return documentNumber; + } + + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java b/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java new file mode 100644 index 00000000..7f70b236 --- /dev/null +++ b/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java @@ -0,0 +1,137 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; + +import ee.sk.smartid.CertificateParser; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.util.StringUtil; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +/** + * Validates and maps the received session status to certificate choice response + */ +public class CertificateChoiceResponseMapper { + + /** + * Maps session status to certificate choice response + *

      + * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @return certificate choice response + */ + public static CertificateChoiceResponse from(SessionStatus sessionStatus) { + return from(sessionStatus, CertificateLevel.QUALIFIED); + } + + /** + * Maps session status to certificate choice response + * + * @param sessionStatus session status received from Smart-ID server + * @param requestedCertificateLevel requested certificate level + * @return certificate choice response + */ + public static CertificateChoiceResponse from(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + validateSessionStatus(sessionStatus); + X509Certificate certificate = getValidatedCertificate(sessionStatus, requestedCertificateLevel); + + var certificateChoiceResponse = new CertificateChoiceResponse(); + certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); + certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); + certificateChoiceResponse.setCertificate(certificate); + certificateChoiceResponse.setCertificateLevel(CertificateLevel.valueOf(sessionStatus.getCert().getCertificateLevel())); + certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return certificateChoiceResponse; + } + + private static void validateSessionStatus(SessionStatus sessionStatus) { + if (sessionStatus == null) { + throw new SmartIdClientException("Session status parameter is not provided"); + } + validateResult(sessionStatus.getResult()); + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Session result parameter is missing"); + } + validateEndResult(sessionResult.getEndResult()); + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); + } + } + + private static void validateEndResult(String endResult) { + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); + } + if (!"OK".equalsIgnoreCase(endResult)) { + ErrorResultHandler.handle(endResult); + } + } + + private static X509Certificate getValidatedCertificate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + X509Certificate certificate = CertificateParser.parseX509Certificate(sessionStatus.getCert().getValue()); + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + throw new UnprocessableSmartIdResponseException("Signer's certificate is not valid", ex); + } + return certificate; + } + + private static void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); + } + if (!isCertificateLevelValid(requestedCertificateLevel.name(), sessionCertificate.getCertificateLevel())) { + throw new CertificateLevelMismatchException("Certificate level returned by Smart-ID is lower than requested"); + } + } + + private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { + CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); + CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); + return returnedLevel.isSameLevelOrHigher(requestedLevel); + } +} diff --git a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java index 8de6222f..464d3613 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 96ad72ec..62ab5d1e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -33,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.v3.rest.SmartIdConnector; import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; @@ -131,16 +132,17 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddr * This response includes essential values such as sessionID, sessionToken, and sessionSecret, * which can be used by the Relying Party to manage and verify the session independently. *

      + * * @return DynamicLinkCertificateChoiceSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. * @throws SmartIdClientException if the response is invalid or missing necessary session data. */ public DynamicLinkSessionResponse initCertificateChoice() { validateParameters(); CertificateChoiceSessionRequest request = createCertificateRequest(); - DynamicLinkSessionResponse response = connector.getCertificate(request); + DynamicLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); if (response == null || response.getSessionID() == null) { - throw new SmartIdClientException("Dynamic link certificate choice session failed: invalid response received."); + throw new UnprocessableSmartIdResponseException("Dynamic link certificate choice session failed: invalid response received."); } return response; } diff --git a/src/main/java/ee/sk/smartid/v3/SignableHash.java b/src/main/java/ee/sk/smartid/v3/SignableHash.java index c90616e9..5da00753 100644 --- a/src/main/java/ee/sk/smartid/v3/SignableHash.java +++ b/src/main/java/ee/sk/smartid/v3/SignableHash.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -72,4 +72,4 @@ public void setHashType(HashType hashType) { public boolean areFieldsFilled() { return hashType != null && hash != null && hash.length > 0; } -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/v3/SingatureResponse.java b/src/main/java/ee/sk/smartid/v3/SignatureResponse.java similarity index 87% rename from src/main/java/ee/sk/smartid/v3/SingatureResponse.java rename to src/main/java/ee/sk/smartid/v3/SignatureResponse.java index a3a091f0..00c39d55 100644 --- a/src/main/java/ee/sk/smartid/v3/SingatureResponse.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,14 +30,11 @@ import java.security.cert.X509Certificate; import java.util.Base64; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -public class SingatureResponse implements Serializable { +public class SignatureResponse implements Serializable { private String endResult; - private String signedHashInBase64; - private HashType hashType; private String signatureValueInBase64; private String algorithmName; private X509Certificate certificate; @@ -96,22 +93,6 @@ public void setCertificateLevel(String certificateLevel) { this.certificateLevel = certificateLevel; } - public String getSignedHashInBase64() { - return signedHashInBase64; - } - - public void setSignedHashInBase64(String signedHashInBase64) { - this.signedHashInBase64 = signedHashInBase64; - } - - public HashType getHashType() { - return hashType; - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - public String getRequestedCertificateLevel() { return requestedCertificateLevel; } diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java index 4d35056d..90fd4a6e 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -52,7 +52,7 @@ public class SignatureResponseMapper { private static final Logger logger = LoggerFactory.getLogger(SignatureResponseMapper.class); /** - * Create {@link SingatureResponse} from {@link SessionStatus} + * Create {@link SignatureResponse} from {@link SessionStatus} * * @param sessionStatus session status response * @param requestedCertificateLevel certificate level used to start the signature session @@ -62,28 +62,27 @@ public class SignatureResponseMapper { * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. */ - public static SingatureResponse from(SessionStatus sessionStatus, + public static SignatureResponse from(SessionStatus sessionStatus, String requestedCertificateLevel - ) throws UserRefusedException, - UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { validateSessionsStatus(sessionStatus, requestedCertificateLevel); SessionResult sessionResult = sessionStatus.getResult(); SessionSignature sessionSignature = sessionStatus.getSignature(); SessionCertificate certificate = sessionStatus.getCert(); - var singatureResponse = new SingatureResponse(); - singatureResponse.setEndResult(sessionResult.getEndResult()); - singatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); - singatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - singatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - singatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); - singatureResponse.setCertificateLevel(certificate.getCertificateLevel()); - singatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - singatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - singatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return singatureResponse; + var signatureResponse = new SignatureResponse(); + signatureResponse.setEndResult(sessionResult.getEndResult()); + signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); + signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); + signatureResponse.setCertificateLevel(certificate.getCertificateLevel()); + signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return signatureResponse; } private static void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { @@ -205,4 +204,4 @@ private static void validateRawDigestSignature(SessionStatus sessionStatus) { logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); } -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java index 1975c795..3dce30ae 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/v3/SmartIdClient.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -138,7 +138,7 @@ public NotificationSignatureSessionRequestBuilder createNotificationSignature() * * @return Sessions status poller */ - public SessionStatusPoller getSessionsStatusPoller() { + public SessionStatusPoller getSessionStatusPoller() { if (sessionStatusPoller == null) { sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); diff --git a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java index 4d5d3d23..93a294f2 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -67,7 +67,7 @@ public SessionStatus fetchFinalSessionStatus(String sessionId) { private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { SessionStatus sessionStatus = null; while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - sessionStatus = getSessionsStatus(sessionId); + sessionStatus = getSessionStatus(sessionId); if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { break; } @@ -84,7 +84,7 @@ private SessionStatus pollForFinalSessionStatus(String sessionId) throws Interru * @param sessionId session id from init session response * @return Sessions status */ - public SessionStatus getSessionsStatus(String sessionId) { + public SessionStatus getSessionStatus(String sessionId) { logger.debug("Querying session status"); return connector.getSessionStatus(sessionId); } diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java index b0242d71..816f05d2 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -67,7 +67,7 @@ public interface SmartIdConnector extends Serializable { * @param request CertificateChoiceSessionRequest containing necessary parameters * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest request); + DynamicLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request); /** * Initiates a notification based certificate choice request. @@ -173,4 +173,4 @@ public interface SmartIdConnector extends Serializable { * @return The notification authentication session response */ NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java index bcc37a24..568edd5f 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java @@ -178,7 +178,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public DynamicLinkSessionResponse getCertificate(CertificateChoiceSessionRequest request) { + public DynamicLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request) { logger.debug("Initiating dynamic link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java index 32e19c58..394cbc29 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,16 +27,25 @@ */ import java.io.Serializable; +import java.time.Instant; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class DynamicLinkSessionResponse implements Serializable { + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private final Instant receivedAt; + private String sessionID; private String sessionToken; private String sessionSecret; + public DynamicLinkSessionResponse() { + receivedAt = Instant.now(); + } + public String getSessionID() { return sessionID; } @@ -60,4 +69,8 @@ public String getSessionSecret() { public void setSessionSecret(String sessionSecret) { this.sessionSecret = sessionSecret; } + + public Instant getReceivedAt() { + return receivedAt; + } } diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java index 7a79f9f9..60c1a678 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -28,6 +28,9 @@ import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class NotificationCertificateChoiceSessionResponse implements Serializable { private String sessionID; diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java index 9096bbe0..96ea5dd5 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java +++ b/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -36,15 +36,15 @@ public NotificationInteraction(NotificationInteractionFlow notificationInteracti } public static NotificationInteraction verificationCodeChoice(String displayText60) { - var interactionDeprecated = new NotificationInteraction(VERIFICATION_CODE_CHOICE); - interactionDeprecated.displayText60 = displayText60; - return interactionDeprecated; + var interaction = new NotificationInteraction(VERIFICATION_CODE_CHOICE); + interaction.displayText60 = displayText60; + return interaction; } public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { - var interactionDeprecated = new NotificationInteraction(CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - interactionDeprecated.displayText200 = displayText200; - return interactionDeprecated; + var interaction = new NotificationInteraction(CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); + interaction.displayText200 = displayText200; + return interaction; } @Override diff --git a/src/test/java/ee/sk/smartid/CertificateParserTest.java b/src/test/java/ee/sk/smartid/CertificateParserTest.java index 9838b1ae..66947565 100644 --- a/src/test/java/ee/sk/smartid/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/CertificateParserTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -31,7 +31,6 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.CertificateParser; public class CertificateParserTest { diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java index f86fdf4b..f4b22cf4 100644 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index ad1d6c5c..a410c50e 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java index c757b008..200a2993 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java index 0b8387f1..1f52f305 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,10 +32,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.cert.CertificateEncodingException; import java.util.Collections; @@ -55,15 +53,15 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.rest.SessionStatusPoller; import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionSignature; import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.SessionStatusPoller; public class AuthenticationRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java index 6bf18586..4ec139c2 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java b/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java index b8c6f4ec..42d5c1f5 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,8 +32,6 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.CertificateLevel; - public class CertificateLevelTest { @Test diff --git a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java index 78efcc71..f58f2ef4 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,10 +29,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.cert.X509Certificate; @@ -52,11 +50,11 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.SessionStatusPoller; import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.SessionCertificate; import ee.sk.smartid.v2.rest.dao.SessionStatus; diff --git a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java index 8b891284..89d5979a 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/v2/CertificateUtil.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java index becda236..be3da970 100644 --- a/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/DummyData.java b/src/test/java/ee/sk/smartid/v2/DummyData.java index 5c735181..d94fdb29 100644 --- a/src/test/java/ee/sk/smartid/v2/DummyData.java +++ b/src/test/java/ee/sk/smartid/v2/DummyData.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java index e83ce6fe..fabf6b8e 100644 --- a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -45,7 +45,7 @@ import ee.sk.smartid.FileUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.integration.SmartIdIntegrationTest; +import ee.sk.smartid.v2.integration.SmartIdIntegrationTest; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; diff --git a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java index e431405c..26175bb5 100644 --- a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,7 +32,6 @@ import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; -import ee.sk.smartid.v2.SignableHash; public class SignableHashTest { diff --git a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java index 0406e314..9133130d 100644 --- a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -33,10 +33,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; @@ -54,13 +52,13 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.v2.rest.SessionStatusPoller; import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; +import ee.sk.smartid.v2.rest.dao.Capability; import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SessionSignature; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.dao.Capability; public class SignatureRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java index 7a4339ce..accd85b5 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java index 63ffd654..0ffefa8d 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -79,11 +79,11 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.SmartIdConnector; import ee.sk.smartid.v2.rest.SmartIdRestConnector; +import ee.sk.smartid.v2.rest.dao.Interaction; +import ee.sk.smartid.v2.rest.dao.SessionStatus; @WireMockTest(httpPort = 18089) class SmartIdClientTest { diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java index 63bd3058..06ef4dd7 100644 --- a/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java +++ b/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -33,7 +33,6 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.v2.SmartIdSignature; public class SmartIdSignatureTest { diff --git a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java index 68828b8e..0401f22a 100644 --- a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/integration/ReadmeTest.java rename to src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java index e3afc41f..17c97610 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeTest.java +++ b/src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.integration; +package ee.sk.smartid.v2.integration; /*- * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -57,23 +57,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.v2.AuthenticationHash; import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.HashType; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.FileUtil; +import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.AuthenticationHash; +import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.v2.SignableHash; import ee.sk.smartid.v2.SmartIdAuthenticationResponse; import ee.sk.smartid.v2.SmartIdCertificate; import ee.sk.smartid.v2.SmartIdClient; import ee.sk.smartid.v2.SmartIdSignature; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.v2.rest.SmartIdConnector; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; @@ -421,7 +421,7 @@ public void documentCreatingSignature() { RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interaction supported by the app is taken from `allowedInteractionsOrder` list and sent to client. -The interaction that was actually used is reported back to RP with interactionUsed response parameter to the session request. +The interaction that was actually used is reported back to RP with interactionFlowUsed response parameter to the session request. If the app cannot support any interaction requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. `displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java rename to src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java index 43aedbcd..cfdc3782 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.integration; +package ee.sk.smartid.v2.integration; /*- * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -42,12 +42,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.smartid.v2.AuthenticationHash; import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.FileUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.AuthenticationHash; +import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.v2.SignableData; import ee.sk.smartid.v2.SmartIdAuthenticationResponse; import ee.sk.smartid.v2.SmartIdCertificate; @@ -76,8 +76,6 @@ public void setUp() { client.setRelyingPartyName(RELYING_PARTY_NAME); client.setHostUrl(HOST_URL); client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - // temporary solution to skip tests going against smart-id demo env } @Test diff --git a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java index 486b6861..08918b4a 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java index d9b18389..c93c1352 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java index 60f66e69..d19e422b 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -57,18 +57,18 @@ import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.ClientRequestHeaderFilter; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v2.ClientRequestHeaderFilter; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java index 8ce94fd9..ec56b265 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -45,13 +45,13 @@ import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; +import ee.sk.smartid.v2.rest.dao.CertificateRequest; +import ee.sk.smartid.v2.rest.dao.Interaction; import ee.sk.smartid.v2.rest.dao.SessionStatus; import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; diff --git a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java index 383916f2..695ca767 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java index 7b8a5e59..f5164dcb 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java +++ b/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java index 35cc44fd..60371d39 100644 --- a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -48,8 +48,8 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import ee.sk.smartid.v2.CertificateUtil; import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.v2.CertificateUtil; public class CertificateAttributeUtilTest { diff --git a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java index eceb9166..ed3cf0c9 100644 --- a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -41,10 +41,10 @@ import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.util.NationalIdentityNumberUtil; import ee.sk.smartid.v2.AuthenticationResponseValidator; import ee.sk.smartid.v2.CertificateUtil; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; public class NationalIdentityNumberUtilTest { diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java similarity index 85% rename from src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java rename to src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java index 93f4d666..5ea7a00a 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -50,7 +50,7 @@ import ee.sk.smartid.v3.rest.dao.SessionSignature; import ee.sk.smartid.v3.rest.dao.SessionStatus; -class DynamicLinkAuthenticationResponseMapperTest { +class AuthenticationResponseMapperTest { private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); @@ -61,27 +61,27 @@ void from() { var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - DynamicLinkAuthenticationResponse dynamicLinkAuthenticationResponse = DynamicLinkAuthenticationResponseMapper.from(sessionStatus); + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - assertEquals("OK", dynamicLinkAuthenticationResponse.getEndResult()); - assertEquals("signatureValue", dynamicLinkAuthenticationResponse.getSignatureValueInBase64()); - assertEquals(toX509Certificate(AUTH_CERT), dynamicLinkAuthenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, dynamicLinkAuthenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", dynamicLinkAuthenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", dynamicLinkAuthenticationResponse.getInteractionFlowUsed()); - assertEquals("0.0.0.0", dynamicLinkAuthenticationResponse.getDeviceIpAddress()); + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionFlowUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); } @Test void from_sessionStatusNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> DynamicLinkAuthenticationResponseMapper.from(null)); + var exception = assertThrows(SmartIdClientException.class, () -> AuthenticationResponseMapper.from(null)); assertEquals("Session status parameter is not provided", exception.getMessage()); } @Test void from_sessionResultIsNotPresent_throwException() { var sessionStatus = new SessionStatus(); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Session result parameter is missing", exception.getMessage()); } @@ -94,7 +94,7 @@ void from_endResultIsNotPresent_throwException(String endResult) { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("End result parameter is missing in the session result", exception.getMessage()); } @@ -106,7 +106,7 @@ void from_endResultIsTimeout_throwException() { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - assertThrows(SessionTimeoutException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + assertThrows(SessionTimeoutException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); } @ParameterizedTest @@ -117,7 +117,7 @@ void from_documentNumberIsEmpty_throwException(String documentNumber) { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Document number parameter is missing in the session result", exception.getMessage()); } @@ -130,7 +130,7 @@ void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol sessionStatus.setResult(sessionResult); sessionStatus.setSignatureProtocol(signatureProtocol); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Signature protocol parameter is missing in session status", exception.getMessage()); } @@ -143,7 +143,7 @@ void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignat sessionStatus.setResult(sessionResult); sessionStatus.setSignatureProtocol(invalidSignatureProtocol); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Invalid signature protocol in sessions status", exception.getMessage()); } @@ -155,7 +155,7 @@ void from_signatureIsNotProvided_throwException() { sessionStatus.setResult(sessionResult); sessionStatus.setSignatureProtocol("ACSP_V1"); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Signature parameter is missing in session status", exception.getMessage()); } @@ -172,7 +172,7 @@ void from_signatureValueIsNotProvided_throwException(String signatureValue) { sessionStatus.setSignatureProtocol("ACSP_V1"); sessionStatus.setSignature(sessionSignature); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Value parameter is missing in signature", exception.getMessage()); } @@ -190,7 +190,7 @@ void from_serverRandomIsNotProvided_throwException(String serverRandom) { sessionStatus.setSignatureProtocol("ACSP_V1"); sessionStatus.setSignature(sessionSignature); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Server random parameter is missing in signature", exception.getMessage()); } @@ -205,7 +205,7 @@ void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorit sessionStatus.setSignatureProtocol("ACSP_V1"); sessionStatus.setSignature(sessionSignature); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Signature algorithm parameter is missing in signature", exception.getMessage()); } @@ -219,7 +219,7 @@ void from_sessionCertificateIsNotProvided_throwException() { sessionStatus.setSignatureProtocol("ACSP_V1"); sessionStatus.setSignature(sessionSignature); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Certificate parameter is missing in session status", exception.getMessage()); } @@ -238,7 +238,7 @@ void from_certificateValueIsNotProvided_throwException(String certificateValue) sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Value parameter is missing in certificate", exception.getMessage()); } @@ -255,7 +255,7 @@ void from_certificateLevelIsNotProvided_throwException(String certificateLevel) sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Certificate level parameter is missing in certificate", exception.getMessage()); } @@ -273,7 +273,7 @@ void from_interactionFlowUsedNotProvided_throwException(String interactionFlowUs sessionStatus.setCert(sessionCertificate); sessionStatus.setInteractionFlowUsed(interactionFlowUsed); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Interaction flow used parameter is missing in the session status", exception.getMessage()); } @@ -290,7 +290,7 @@ void from_certificateIsInvalid_throwException() { sessionStatus.setCert(sessionCertificate); sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); - var exception = assertThrows(SmartIdClientException.class, () -> DynamicLinkAuthenticationResponseMapper.from(sessionStatus)); + var exception = assertThrows(SmartIdClientException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); } diff --git a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java index b683725b..3dafb025 100644 --- a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -66,7 +66,7 @@ void setUp() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); @@ -94,7 +94,7 @@ void toAuthenticationIdentity() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); @@ -124,7 +124,7 @@ void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidateCertificateLevel_ok() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); @@ -160,7 +160,7 @@ void toAuthenticationIdentity_dynamicLinkAuthenticationResponseIsMissing_throwEx @Test void toAuthenticationIdentity_randomChallengeIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, null)); assertEquals("Random challenge is not provided", exception.getMessage()); @@ -168,7 +168,7 @@ void toAuthenticationIdentity_randomChallengeIsNotProvided_throwException() { @Test void toAuthenticationIdentity_certificateValueIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); assertEquals("Certificate is not provided", exception.getMessage()); @@ -176,7 +176,7 @@ void toAuthenticationIdentity_certificateValueIsNotProvided_throwException() { @Test void toAuthenticationIdentity_expiredCertificateProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(EXPIRED_CERT)); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); @@ -185,7 +185,7 @@ void toAuthenticationIdentity_expiredCertificateProvided_throwException() { @Test void toAuthenticationIdentity_certificateIsNotTrusted_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); @@ -194,7 +194,7 @@ void toAuthenticationIdentity_certificateIsNotTrusted_throwException() { @Test void toAuthenticationIdentity_certificateLevelIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(null); var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); @@ -204,7 +204,7 @@ void toAuthenticationIdentity_certificateLevelIsNotProvided_throwException() { @Test void toAuthenticationIdentity_certificateLevelIsLowerThanRequested_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.ADVANCED); var exception = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); @@ -214,7 +214,7 @@ void toAuthenticationIdentity_certificateLevelIsLowerThanRequested_throwExceptio @Test void toAuthenticationIdentity_algorithmNameIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); @@ -224,7 +224,7 @@ void toAuthenticationIdentity_algorithmNameIsNotProvided_throwException() { @Test void toAuthenticationIdentity_signatureValueIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); @@ -235,7 +235,7 @@ void toAuthenticationIdentity_signatureValueIsNotProvided_throwException() { @Test void toAuthenticationIdentity_invalidAlgorithmNameIsProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName("invalidAlgorithmName"); @@ -247,7 +247,7 @@ void toAuthenticationIdentity_invalidAlgorithmNameIsProvided_throwException() { @Test void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); @@ -260,7 +260,7 @@ void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_signatureDoesNotMatch_throwException() { - var dynamicLinkAuthenticationResponse = new DynamicLinkAuthenticationResponse(); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); diff --git a/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java new file mode 100644 index 00000000..08e0ff7f --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java @@ -0,0 +1,243 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.v3.rest.dao.SessionCertificate; +import ee.sk.smartid.v3.rest.dao.SessionResult; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + +public class CertificateChoiceResponseMapperTest { + + private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + @Test + void from() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); + sessionCertificate.setCertificateLevel("QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(toX509Certificate(), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void from_returnedCertificateHigherThanRequested_ok() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); + sessionCertificate.setCertificateLevel("QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(toX509Certificate(), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void from_expiredCertificateWasReturned() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(getEncodedCertificateData(EXPIRED_CERT)); + sessionCertificate.setCertificateLevel("QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Signer's certificate is not valid", ex.getMessage()); + } + + @Test + void from_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); + sessionCertificate.setCertificateLevel("ADVANCED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Certificate level returned by Smart-ID is lower than requested", ex.getMessage()); + } + + @Test + void from_sessionCertificateLevelIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Certificate level parameter is missing in certificate", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Value parameter is missing in certificate", ex.getMessage()); + } + + @Test + void from_sessionCertificateIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Certificate parameter is missing in session status", ex.getMessage()); + } + + @Test + void from_sessionDocumentNumberIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("Document number parameter is missing in the session result", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(expectedException, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + } + + @Test + void from_sessionEndResultIsNotProvided_throwException() { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(new SessionResult()); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + assertEquals("End result parameter is missing in the session result", ex.getMessage()); + } + + @Test + void from_sessionResultIsNotProvided_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(new SessionStatus())); + assertEquals("Session result parameter is missing", ex.getMessage()); + } + + @Test + void from_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> CertificateChoiceResponseMapper.from(null)); + assertEquals("Session status parameter is not provided", ex.getMessage()); + } + + private static X509Certificate toX509Certificate() { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(CERTIFICATE_CHOICE_CERT.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + private static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java index 21effebf..223546b8 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -38,6 +38,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.v3.rest.SmartIdConnector; @@ -62,7 +63,7 @@ void setUp() { @Test void initiateCertificateChoice() { - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DynamicLinkSessionResponse result = builderService.initCertificateChoice(); @@ -71,13 +72,13 @@ void initiateCertificateChoice() { assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullRequestProperties() { builderService.withShareMdClientIpAddress(false); - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DynamicLinkSessionResponse result = builderService.initCertificateChoice(); @@ -86,24 +87,24 @@ void initiateCertificateChoice_nullRequestProperties() { assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_missingCertificateLevel() { builderService.withCertificateLevel(null); - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DynamicLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_withValidCapabilities() { builderService.withCapabilities("ADVANCED", "QUALIFIED"); - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DynamicLinkSessionResponse result = builderService.initCertificateChoice(); @@ -112,13 +113,13 @@ void initiateCertificateChoice_withValidCapabilities() { assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullCapabilities() { builderService.withCapabilities(); - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DynamicLinkSessionResponse result = builderService.initCertificateChoice(); @@ -127,7 +128,7 @@ void initiateCertificateChoice_nullCapabilities() { assertEquals("test-session-token", result.getSessionToken()); assertEquals("test-session-secret", result.getSessionSecret()); - verify(connector).getCertificate(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @Nested @@ -135,9 +136,9 @@ class ErrorCases { @Test void initiateCertificateChoice_whenResponseIsNull() { - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(null); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(null); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); } @@ -146,15 +147,15 @@ void initiateCertificateChoice_whenSessionIDIsNull() { var responseWithNullSessionID = new DynamicLinkSessionResponse(); responseWithNullSessionID.setSessionToken("test-session-token"); responseWithNullSessionID.setSessionSecret("test-session-secret"); - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); } @Test void initiateCertificateChoice_userAccountNotFound() { - when(connector.getCertificate(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); + when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); assertEquals(UserAccountNotFoundException.class, ex.getClass()); diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java index 0b247819..bfd6c768 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java index c2f9cf3f..fc96c859 100644 --- a/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java +++ b/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,27 +29,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.util.stream.Stream; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; class ErrorResultHandlerTest { @@ -71,21 +56,4 @@ void handle_unknownEndResult(String unknownEndResult) { var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(unknownEndResult)); assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); } - - static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("USER_REFUSED", UserRefusedException.class), - Arguments.of("TIMEOUT", SessionTimeoutException.class), - Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), - Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), - Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), - Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); - } - } } diff --git a/src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java new file mode 100644 index 00000000..dd17bcb9 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java @@ -0,0 +1,65 @@ +package ee.sk.smartid.v3; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; + +public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), + Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), + Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java index 0c37f740..b952ebe4 100644 --- a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,30 +30,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.stream.Stream; - import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.v3.rest.dao.SessionCertificate; import ee.sk.smartid.v3.rest.dao.SessionResult; import ee.sk.smartid.v3.rest.dao.SessionSignature; @@ -61,8 +46,7 @@ class SignatureResponseMapperTest { - // TODO - 10.12.24: replace this with signing certificate - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); @Test void from_stateParameterMissing() { @@ -191,7 +175,7 @@ void from_validRawDigestSignature() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - SingatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); + SignatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); assertEquals("OK", response.getEndResult()); } @@ -288,27 +272,8 @@ private static SessionStatus createMockSessionStatus(String signatureProtocol, S } private static String getEncodedCertificateData() { - return SignatureResponseMapperTest.AUTH_CERT.replace("-----BEGIN CERTIFICATE-----", "") + return SignatureResponseMapperTest.SIGN_CERT.replace("-----BEGIN CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "") .replace("\n", ""); } - - private static class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("USER_REFUSED", UserRefusedException.class), - Arguments.of("TIMEOUT", SessionTimeoutException.class), - Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), - Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), - Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), - Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), - Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) - ); - } - } -} \ No newline at end of file +} diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java index 3537ec64..7e1a957a 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -77,8 +77,7 @@ class DynamicLinkCertificateChoiceSession { @Test void createDynamicLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -90,6 +89,7 @@ void createDynamicLinkCertificateChoice() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } } @@ -131,6 +131,7 @@ class DynamicLinkAuthenticationSession { @Test void createDynamicLinkAuthentication_anonymous() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) @@ -140,11 +141,13 @@ void createDynamicLinkAuthentication_anonymous() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } @Test void createDynamicLinkAuthentication_withDocumentNumber() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -155,11 +158,13 @@ void createDynamicLinkAuthentication_withDocumentNumber() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } @Test void createDynamicLinkAuthentication_withSemanticsIdentifier() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -170,6 +175,7 @@ void createDynamicLinkAuthentication_withSemanticsIdentifier() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } } @@ -179,7 +185,7 @@ class DynamicLinkSignatureSession { @Test void createDynamicLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); @@ -195,11 +201,12 @@ void createDynamicLinkSignature_withDocumentNumber() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } @Test void createDynamicLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); @@ -215,6 +222,7 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getReceivedAt()); } } @@ -309,9 +317,9 @@ class SessionsStatus { @Test void fetchFinalSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-successful-authentication.json"); - SessionStatus status = smartIdClient.getSessionsStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); + SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); assertEquals("COMPLETE", status.getState()); assertEquals("OK", status.getResult().getEndResult()); @@ -321,7 +329,7 @@ void fetchFinalSessionStatus() { void getSessionStatus() { SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-running.json"); - SessionStatus status = smartIdClient.getSessionsStatusPoller().getSessionsStatus("abcdef1234567890"); + SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); assertEquals("RUNNING", status.getState()); assertNull(status.getResult()); @@ -336,19 +344,20 @@ class DynamicContent { @EnumSource void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); - Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, 1, response.getSessionSecret()); + long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() .withDynamicLinkType(dynamicLinkType) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(response.getSessionToken()) - .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withElapsedSeconds(elapsedSeconds) .withUserLanguage("eng") .withAuthCode(authCode) .createUri(); @@ -359,21 +368,20 @@ void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLin @ParameterizedTest @EnumSource void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); - Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, 1, response.getSessionSecret()); + long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() .withDynamicLinkType(dynamicLinkType) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) - .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withElapsedSeconds(elapsedSeconds) .withUserLanguage("eng") .withAuthCode(authCode) .createUri(); @@ -383,21 +391,20 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-response.json"); - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-ok.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); - Instant sessionResponseReceivedTime = Instant.now(); - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, 1, response.getSessionSecret()); + long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); String qrCodeDataUri = smartIdClient.createDynamicContent() .withDynamicLinkType(DynamicLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) - .withElapsedSeconds(Duration.between(sessionResponseReceivedTime, Instant.now()).getSeconds()) + .withElapsedSeconds(elapsedSeconds) .withUserLanguage("eng") .withAuthCode(authCode) .createQrCodeDataUri(); @@ -421,4 +428,4 @@ private static void assertUri(URI qrCodeUri, SessionType sessionType, DynamicLin assertTrue(qrCodeUri.getQuery().contains("authCode=")); } } -} \ No newline at end of file +} diff --git a/src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java new file mode 100644 index 00000000..8dc7f7e0 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java @@ -0,0 +1,763 @@ +package ee.sk.smartid.v3.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.HashType; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.AuthCode; +import ee.sk.smartid.v3.AuthenticationCertificateLevel; +import ee.sk.smartid.v3.AuthenticationResponse; +import ee.sk.smartid.v3.AuthenticationResponseMapper; +import ee.sk.smartid.v3.AuthenticationResponseValidator; +import ee.sk.smartid.v3.CertificateChoiceResponse; +import ee.sk.smartid.v3.CertificateChoiceResponseMapper; +import ee.sk.smartid.v3.CertificateLevel; +import ee.sk.smartid.v3.DynamicLinkType; +import ee.sk.smartid.v3.RandomChallenge; +import ee.sk.smartid.v3.SessionType; +import ee.sk.smartid.v3.SignableData; +import ee.sk.smartid.v3.SignatureResponse; +import ee.sk.smartid.v3.SignatureResponseMapper; +import ee.sk.smartid.v3.SmartIdClient; +import ee.sk.smartid.v3.rest.SessionStatusPoller; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.SessionStatus; + + +@Disabled("Replace relying party UUID and name with your own values in setup") +@SmartIdDemoIntegrationTest +public class ReadmeIntegrationTest { + + private static final String ALPHA_NUMERIC_PATTERN = "^[A-z0-9]{4}$"; + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + KeyStore keyStore = getKeystore(); + smartIdClient.setTrustStore(keyStore); + } + + @Disabled("Demo user account for full dynamic-link flow is not yet available") + @Nested + class DynamicLinkExamples { + + @Test + void anonymousAuthentication_withApp2App() { + // For security reasons a new hash value must be created for each new authentication request + String randomChallenge = RandomChallenge.generate(); + // Store generated randomChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDynamicLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + // before the user can enter PIN. If user selects wrong verification code then the operation will fail. + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) + .initAuthenticationSession(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + + // Will be used to calculate elapsed time being used in dynamic link and in authCode + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Generate auth code + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + // Generate dynamic link + URI dynamicLink = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link + .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for + .withSessionToken(sessionToken) // provide token from sessions response + .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time + .withAuthCode(authCode) + .createUri(); + // Return dynamic-link to the frontend to be used by the user. + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get sessionID from current session response and poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + + assertEquals("COMPLETE", sessionStatus.getState()); + + // validate sessions status result and map session status to authentication response + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // validate certificate value and signature and map it to authentication identity + var authenticationResponseValidator = new AuthenticationResponseValidator(); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifierAndQrCode() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new random challenge must be created for each new authentication request + String randomChallenge = RandomChallenge.generate(); + // Store generated randomChallenge only backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDynamicLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Generate auth code + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) + String qrCodeDataUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(sessionToken) // provide token from sessions response + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(authCode) + .createQrCodeDataUri(); + // Display QR-code to the user + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get sessionID from current session response and poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETED", sessionStatus.getState()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + + // validate sessions status result and map session status to authentication response + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // validate certificate value and signature and map it to authentication identity + var authenticationResponseValidator = new AuthenticationResponseValidator(); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, "randomChallenge"); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withDocumentNumberAndQrCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new random challenge must be created for each new authentication request + String randomChallenge = RandomChallenge.generate(); + // Store generated randomChallenge only backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDynamicLinkAuthentication() + .withDocumentNumber(documentNumber) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withRandomChallenge(randomChallenge) + .withAllowedInteractionsOrder(Collections.singletonList( + DynamicLinkInteraction.displayTextAndPIN("Log in?") + )) + // we want to get the IP address of the device running Smart-ID app + // for the IP to be returned the service provider (SK) must switch on this option + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Generate auth code + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) + String qrCodeDataUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for + .withSessionToken(sessionToken) // provide token from sessions response + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(authCode) + .createQrCodeDataUri(); + // Display QR-code to the user + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get sessionID from current session response and poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", sessionStatus.getState()); + + assertEquals("OK", sessionStatus.getResult().getEndResult()); + System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); + // validate sessions status result and map session status to authentication response + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // validate certificate value and signature and map it to authentication identity + var authenticationResponseValidator = new AuthenticationResponseValidator(); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, "randomChallenge"); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Test + void signature_withDocumentNumber() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + + // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA512); + + // Build the dynamic link signature request + DynamicLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.getSessionID(); + String sessionToken = signatureSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.getSessionSecret(); + Instant receivedAt = signatureSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); + // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) + String qrCodeDataUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for + .withSessionToken(sessionToken) // provide token from sessions response + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(authCode) + .createQrCodeDataUri(); + // Display QR-code to the user + + // Get the session status poller + poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withSemanticIdentifier() { + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withDocumentNumber("PNOLT-40504040001-MOCK-Q") + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + + // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA512); + + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Build the dynamic link signature request + DynamicLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withAllowedInteractionsOrder(List.of( + DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.getSessionID(); + String sessionToken = signatureSessionResponse.getSessionToken(); + + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.getSessionSecret(); + Instant receivedAt = signatureSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); + // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) + String qrCodeDataUri = smartIdClient.createDynamicContent() + .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for + .withSessionToken(sessionToken) // provide token from sessions response + .withElapsedSeconds(elapsedSeconds) + .withAuthCode(authCode) + .createQrCodeDataUri(); + // Display QR-code to the user + + // Get the session status poller + poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + + @Nested + class NotificationBasedExamples { + + @Test + void authentication_withDocumentNumber() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new hash value must be created for each new authentication request + String randomChallenge = RandomChallenge.generate(); + // Store generated randomChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + NotificationInteraction.verificationCodeChoice("Log in?") + )) + .initAuthenticationSession(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String verificationCode = authenticationSessionResponse.getVc().getValue(); + // Display the verification code to the user for confirmation + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get sessionID from current session response + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + + // validate sessions status result and map session status to authentication response + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // validate certificate value and signature and map it to authentication identity + var authenticationResponseValidator = new AuthenticationResponseValidator(); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new hash value must be created for each new authentication request + String randomChallenge = RandomChallenge.generate(); + // Store generated randomChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient + .createNotificationAuthentication() + .withSemanticsIdentifier(semanticIdentifier) + .withRandomChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withAllowedInteractionsOrder(Collections.singletonList( + NotificationInteraction.verificationCodeChoice("Log in?") + )) + .initAuthenticationSession(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String verificationCode = authenticationSessionResponse.getVc().getValue(); + // Display the verification code to the user for confirmation + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get sessionID from current session response + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + + // validate sessions status result and map session status to authentication response + AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // validate certificate value and signature and map it to authentication identity + var authenticationResponseValidator = new AuthenticationResponseValidator(); + // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + + // validate certificate value and signature and map it to authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void certificateChoice_withDocumentNumber() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; // returned in authentication result and used for re-authentication + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String sessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus sessionStatus = poller.getSessionStatus(sessionId); + CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); + assertNotNull(response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void certificateChoice_withSemanticIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String sessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus sessionStatus = poller.getSessionStatus(sessionId); + + CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); + assertEquals("OK", response.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); + assertNotNull(response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void signature_withDocumentNumber(){ + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withDocumentNumber(documentNumber) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + // For example use SignatureBuilder from digidoc4j to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA512); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + NotificationInteraction.verificationCodeChoice("Please sign the document")) + ) + .initSignatureSession(); + + // Process the querying sessions status response + String sessionID = signatureSessionResponse.getSessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.getVc().getValue(); + assertTrue(Pattern.matches(ALPHA_NUMERIC_PATTERN, verificationCode)); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("verificationCodeChoice", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withSemanticsIdentifier(){ + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withDocumentNumber("PNOEE-40504040001-MOCK-Q") + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA512); + + // Create the Semantics Identifier + var semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" + ); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withAllowedInteractionsOrder(List.of( + NotificationInteraction.verificationCodeChoice("Please sign the document")) + ) + .initSignatureSession(); + + // Process the querying sessions status response + String sessionID = signatureSessionResponse.getSessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.getVc().getValue(); + assertTrue(Pattern.matches(ALPHA_NUMERIC_PATTERN, verificationCode)); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("verificationCodeChoice", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + + private static KeyStore getKeystore() { + try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, "changeit".toCharArray()); + return keyStore; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot find demo truststore", e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java new file mode 100644 index 00000000..8b7f3a24 --- /dev/null +++ b/src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java @@ -0,0 +1,322 @@ +package ee.sk.smartid.v3.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.regex.Pattern; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashType; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.v3.RandomChallenge; +import ee.sk.smartid.v3.SignatureAlgorithm; +import ee.sk.smartid.v3.rest.SmartIdConnector; +import ee.sk.smartid.v3.rest.SmartIdRestConnector; +import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.v3.rest.dao.NotificationInteraction; +import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; + +@Disabled("Relying party demo account not yet available for v3") +@SmartIdDemoIntegrationTest +class SmartIdRestIntegrationTest { + + // Replace these to test with V3 + private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + + private static final String UUID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; + private static final String VERIFICATION_CODE_PATTERN = "^[A-Za-z0-9]{4}$"; + private static final String SESSION_TOKEN_PATTERN = "^[A-Za-z0-9]{24}$"; + private static final String SESSION_SECRET_PATTERN = "^[A-Za-z0-9+/]{24}$"; + + private SmartIdConnector smartIdConnector; + + @BeforeEach + void setUp() { + smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); + } + + @Disabled("Demo account for dynamic-link requests not yet available") + @Nested + class DynamicLink { + + @Nested + class Authentication { + + @Test + void initAnonymousDynamicLinkAuthentication() { + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getReceivedAt()); + } + + @Test + void initDynamicLinkAuthentication_withDocumentNumber() { + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getReceivedAt()); + } + + @Test + void initDynamicLinkAuthentication_withSemanticsIdentifier() { + AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + + DynamicLinkSessionResponse sessionResponse = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionResponse.getSessionSecret())); + assertNotNull(sessionResponse.getReceivedAt()); + } + + private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + var request = new AuthenticationSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + + var signatureParameters = new AcspV1SignatureProtocolParameters(); + signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureParameters.setRandomChallenge(RandomChallenge.generate()); + request.setSignatureProtocolParameters(signatureParameters); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); + return request; + } + } + + @Disabled("Endpoint not yet available") + @Nested + class CertificateChoice { + + @Test + void initDynamicLinkCertificateChoice() { + var request = new CertificateChoiceSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + + DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkCertificateChoice(request); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getReceivedAt()); + } + } + + @Nested + class Signature { + + @Test + void initDynamicLinkSignature_withSemanticIdentifier() { + var request = new SignatureSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign it!"))); + + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); + signatureProtocolParameters.setDigest(digest); + request.setSignatureProtocolParameters(signatureProtocolParameters); + + DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getReceivedAt()); + } + + @Test + void initDynamicLinkSignature_withDocumentNumber() { + var request = new SignatureSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign it!"))); + + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); + signatureProtocolParameters.setDigest(digest); + request.setSignatureProtocolParameters(signatureProtocolParameters); + + DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getReceivedAt()); + } + } + } + + @Nested + class NotificationBasedRequests { + + @Nested + class Authentication { + + @Test + void initNotificationAuthentication_withSemanticIdentifier() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); + assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + } + + @Test + void initNotificationAuthentication_withDocumentNumber() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); + assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + } + + private static AuthenticationSessionRequest toAuthenticationRequest() { + var request = new AuthenticationSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setCertificateLevel("QUALIFIED"); + + String randomChallenge = RandomChallenge.generate(); + var signatureParameters = new AcspV1SignatureProtocolParameters(); + signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureParameters.setRandomChallenge(randomChallenge); + request.setSignatureProtocolParameters(signatureParameters); + + request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Log in?"))); + + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(true); + request.setRequestProperties(requestProperties); + return request; + } + } + + @Nested + class CertificateChoice { + + @Test + void initNotificationCertificateChoice_withSemanticIdentifier() { + var request = new CertificateChoiceSessionRequest(); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + + NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + } + + @Test + void initNotificationCertificateChoice_withDocumentNumber() { + var request = new CertificateChoiceSessionRequest(); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + + NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + } + } + + @Nested + class Signature { + + @Test + void initNotificationSignature_withSemanticIdentifier() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); + assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + } + + @Test + void initNotificationCertificateChoice_withDocumentNumber() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); + assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + } + + private static SignatureSessionRequest toSignatureSessionRequest() { + var request = new SignatureSessionRequest(); + request.setRelyingPartyUUID(RELYING_PARTY_UUID); + request.setRelyingPartyName(RELYING_PARTY_NAME); + request.setCertificateLevel("QUALIFIED"); + + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); + String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureProtocolParameters.setDigest(digest); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Sign it!"))); + return request; + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java index 1b65683b..a61b3af9 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -68,12 +68,12 @@ void fetchFinalSessionStatus() { } @Test - void getSessionsStatus() { + void getSessionStatus() { SessionStatus sessionStatus = new SessionStatus(); sessionStatus.setState("RUNNING"); when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); - SessionStatus sessionsStatus = poller.getSessionsStatus("00000000-0000-0000-0000-000000000000"); + SessionStatus sessionsStatus = poller.getSessionStatus("00000000-0000-0000-0000-000000000000"); assertEquals("RUNNING", sessionsStatus.getState()); assertNull(sessionsStatus.getResult()); diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java index a9cbbe29..a40a55f1 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -39,7 +39,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Instant; import java.util.List; import java.util.concurrent.TimeUnit; @@ -74,10 +76,14 @@ class SmartIdRestConnectorTest { + private static final String SESSION_SECRET = "c2Vzc2lvblNlY3JldA=="; + @Nested @WireMockTest(httpPort = 18089) class SessionStatusTests { + private static final String SERVER_RANDOM = "J0iyCYOu8cTWuoD8rD05IIrZ"; + private SmartIdRestConnector connector; @BeforeEach @@ -87,14 +93,14 @@ void setUp() { @Test void getSessionStatus_running() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunning.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-running.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); } @Test void getSessionStatus_running_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunningWithIgnoredProperties.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-running-with-ignored-properties.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); assertNotNull(sessionStatus.getIgnoredProperties()); @@ -103,18 +109,52 @@ void getSessionStatus_running_withIgnoredProperties() { assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); } + @Test + void getSessionStatus_forSuccessfulAuthenticationRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-authentication.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); + + assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + assertNotNull(sessionStatus.getSignature()); + assertThat(sessionStatus.getSignature().getValue(), startsWith("TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZ")); + assertEquals("sha512WithRSAEncryption", sessionStatus.getSignature().getSignatureAlgorithm()); + assertEquals(SERVER_RANDOM, sessionStatus.getSignature().getServerRandom()); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + @Test void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-certificate-choice.json"); assertSuccessfulResponse(sessionStatus); + assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_forSuccessfulSignatureRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-signature.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); + + assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); + assertNotNull(sessionStatus.getSignature()); + assertThat(sessionStatus.getSignature().getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); + assertEquals("sha256WithRSAEncryption", sessionStatus.getSignature().getSignatureAlgorithm()); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); } @Test void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-authentication.json"); assertSuccessfulResponse(sessionStatus); verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) @@ -124,7 +164,7 @@ void getSessionStatus_hasUserAgentHeader() { @Test void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v3/responses/session-status-successful-authentication.json"); connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); assertSuccessfulResponse(sessionStatus); @@ -141,74 +181,82 @@ void getSessionStatus_whenSessionNotFound() { @Test void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-confirmation.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); } @Test void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-confirmation-vc-choice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); } @Test void getSessionStatus_userHasRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-display-text-and-pin.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); } @Test void getSessionStatus_userHasRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-vc-choice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); } + @Test + void getSessionStatus_userHasRefusedCertChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-cert-choice.json"); + assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CERT_CHOICE"); + } + @Test void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenTimeout.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-timeout.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); } @Test void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-wrong-vc.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); } @Test void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenDocumentUnusable.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-document-unusable.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); } - private void assertSuccessfulResponse(SessionStatus sessionStatus) { + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); + return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + } + + private static void assertSuccessfulResponse(SessionStatus sessionStatus) { assertEquals("COMPLETE", sessionStatus.getState()); assertNotNull(sessionStatus.getResult()); assertEquals("OK", sessionStatus.getResult().getEndResult()); - assertEquals("PNOEE-31111111111", sessionStatus.getResult().getDocumentNumber()); + assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); } - private void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { + private static void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { assertEquals("COMPLETE", sessionStatus.getState()); assertEquals(endResult, sessionStatus.getResult().getEndResult()); } - - private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); - return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } } @Nested @WireMockTest(httpPort = 18082) class SemanticsIdentifierDynamicLinkAuthentication { + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/dynamic-link/etsi/PNOEE-48010010101"; + private SmartIdRestConnector connector; @BeforeEach @@ -218,16 +266,19 @@ void setUp() { @Test void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + + Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + Instant end = Instant.now(); - assertNotNull(response); + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test void initDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -235,7 +286,7 @@ void initDynamicLinkAuthentication_userAccountNotFound_throwException() { @Test void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/etsi/PNOEE-48010010101", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -245,6 +296,8 @@ void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18083) class DocumentNumberDynamicLinkAuthentication { + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q"; + private SmartIdRestConnector connector; @BeforeEach @@ -254,16 +307,22 @@ void setUp() { @Test void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "v3/requests/dynamic-link-authentication-session-request.json", + "v3/responses/dynamic-link-authentication-session-response.json"); + + Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - assertNotNull(response); } @Test void initDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -271,7 +330,7 @@ void initDynamicLinkAuthentication_userAccountNotFound_throwException() { @Test void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -281,6 +340,8 @@ void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18081) class AnonymousDynamicLinkAuthentication { + private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/dynamic-link/anonymous"; + private SmartIdRestConnector connector; @BeforeEach @@ -290,16 +351,19 @@ void setUp() { @Test void initAnonymousDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + + Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + Instant end = Instant.now(); - assertNotNull(response); + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); }); } @@ -307,7 +371,7 @@ void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() @Test void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); }); } @@ -317,6 +381,8 @@ void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException @WireMockTest(httpPort = 18082) class SemanticsIdentifierNotificationAuthentication { + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; + private SmartIdRestConnector connector; @BeforeEach @@ -326,7 +392,7 @@ void setUp() { @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); assertNotNull(response); @@ -335,7 +401,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -343,7 +409,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/notification/etsi/PNOEE-48010010101", "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -353,6 +419,8 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18083) class DocumentNumberNotificationAuthentication { + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; + private SmartIdRestConnector connector; @BeforeEach @@ -362,7 +430,7 @@ void setUp() { @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); assertNotNull(response); @@ -371,7 +439,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -379,7 +447,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/authentication/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -389,6 +457,8 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18089) class DynamicLinkCertificateChoiceTests { + private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/certificatechoice/dynamic-link/anonymous"; + private SmartIdConnector connector; @BeforeEach @@ -397,102 +467,101 @@ public void setUp() { } @Test - void getCertificate() { - stubPostRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/responses/dynamic-link-certificate-choice-response.json"); + void initDynamicLinkCertificateChoice() { + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "v3/responses/dynamic-link-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - DynamicLinkSessionResponse response = connector.getCertificate(request); + Instant start = Instant.now(); + DynamicLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); + Instant end = Instant.now(); - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); - assertEquals("sampleSessionToken", response.getSessionToken()); - assertEquals("sampleSessionSecret", response.getSessionSecret()); + assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); } @Test - void getCertificate_invalidCertificateLevel_throwsBadRequestException() { + void initDynamicLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); request.setCertificateLevel("INVALID_LEVEL"); - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_userAccountNotFound() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 404); + void initDynamicLinkCertificateChoice_userAccountNotFound() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> connector.getCertificate(request)); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_relyingPartyNoPermission() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 403); + void initDynamicLinkCertificateChoice_relyingPartyNoPermission() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_invalidRequest() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 400); + void initDynamicLinkCertificateChoice_invalidRequest() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); CertificateChoiceSessionRequest request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID(""); request.setRelyingPartyName(""); - assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 401); + void initDynamicLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.getCertificate(request)); + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkCertificateChoice(request)); assertEquals("Request is unauthorized for URI http://localhost:18089/certificatechoice/dynamic-link/anonymous", exception.getMessage()); } @Test - void getCertificate_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 471); + void initDynamicLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.getCertificate(request)); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 472); + void initDynamicLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.getCertificate(request)); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } @Test - void getCertificate_throwsSmartIdClientException() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 480); + void initDynamicLinkCertificateChoice_throwsSmartIdClientException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(SmartIdClientException.class, () -> connector.getCertificate(request)); + Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @Test - void getCertificate_throwsServerMaintenanceException() { - stubPostErrorResponse("/certificatechoice/dynamic-link/anonymous", 580); + void initDynamicLinkCertificateChoice_throwsServerMaintenanceException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(ServerMaintenanceException.class, () -> connector.getCertificate(request)); + assertThrows(ServerMaintenanceException.class, () -> connector.initDynamicLinkCertificateChoice(request)); } } @@ -500,6 +569,8 @@ void getCertificate_throwsServerMaintenanceException() { @WireMockTest(httpPort = 18089) class SemanticsIdentifierNotificationCertificateChoiceTests { + private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/certificatechoice/notification/etsi/PNOEE-31111111111"; + private SmartIdRestConnector connector; @BeforeEach @@ -510,7 +581,7 @@ public void setUp() { @Test void initCertificateChoice_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/responses/notification-certificate-choice-session-response.json"); + stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/responses/notification-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -518,13 +589,13 @@ void initCertificateChoice_withSemanticsIdentifier_successful() { NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); assertNotNull(response); - assertEquals("test-session-id", response.getSessionID()); + assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); } @Test void initCertificateChoice_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } @@ -532,7 +603,7 @@ void initCertificateChoice_userAccountNotFound_throwException() { @Test void initCertificateChoice_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/certificatechoice/notification/etsi/PNOEE-31111111111", "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } @@ -542,6 +613,8 @@ void initCertificateChoice_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18089) class DocumentNumberNotificationCertificateChoiceTests { + private static final String CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH = "/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q"; + private SmartIdRestConnector connector; @BeforeEach @@ -552,7 +625,7 @@ public void setUp() { @Test void initCertificateChoice_withDocumentNumber_successful() { - stubPostRequestWithResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/responses/notification-certificate-choice-session-response.json"); + stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/responses/notification-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); String documentNumber = "PNOEE-48010010101-MOCK-Q"; @@ -560,13 +633,13 @@ void initCertificateChoice_withDocumentNumber_successful() { NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, documentNumber); assertNotNull(response); - assertEquals("test-session-id", response.getSessionID()); + assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); } @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -574,7 +647,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse("/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -584,6 +657,8 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18089) class DynamicLinkSignatureTests { + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/dynamic-link/etsi/PNOEE-31111111111"; + private SmartIdRestConnector connector; @BeforeEach @@ -594,7 +669,7 @@ public void setUp() { @Test void initDynamicLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/responses/dynamic-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -609,10 +684,10 @@ void initDynamicLinkSignature_withSemanticsIdentifier_successful() { @Test void initDynamicLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111", "v3/responses/dynamic-link-signature-response.json"); + stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111-MOCK-Q", "v3/responses/dynamic-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); - String documentNumber = "PNOEE-31111111111"; + String documentNumber = "PNOEE-31111111111-MOCK-Q"; DynamicLinkSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); @@ -624,7 +699,7 @@ void initDynamicLinkSignature_withDocumentNumber_successful() { @Test void initDynamicLinkSignature_userAccountNotFound() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 404); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -634,7 +709,7 @@ void initDynamicLinkSignature_userAccountNotFound() { @Test void initDynamicLinkSignature_relyingPartyNoPermission() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 403); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -644,7 +719,7 @@ void initDynamicLinkSignature_relyingPartyNoPermission() { @Test void initDynamicLinkSignature_invalidRequest() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 400); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); SignatureSessionRequest request = new SignatureSessionRequest(); request.setRelyingPartyUUID(""); @@ -656,7 +731,7 @@ void initDynamicLinkSignature_invalidRequest() { @Test void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 401); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -668,7 +743,7 @@ void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_wh @Test void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 471); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -678,7 +753,7 @@ void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcepti @Test void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 472); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -688,7 +763,7 @@ void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { @Test void initDynamicLinkSignature_throwsSmartIdClientException() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 480); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -700,7 +775,7 @@ void initDynamicLinkSignature_throwsSmartIdClientException() { @Test void initDynamicLinkSignature_throwsServerMaintenanceException() { - stubPostErrorResponse("/signature/dynamic-link/etsi/PNOEE-31111111111", 580); + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -713,6 +788,8 @@ void initDynamicLinkSignature_throwsServerMaintenanceException() { @WireMockTest(httpPort = 18084) class SemanticsIdentifierNotificationSignature { + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; + private SmartIdRestConnector connector; @BeforeEach @@ -722,7 +799,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -738,7 +815,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -750,7 +827,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse("/signature/notification/etsi/PNOEE-48010010101", "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -762,7 +839,7 @@ void initNotificationSignature_requestIsUnauthorized_throwException() { @Test void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 471); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -774,7 +851,7 @@ void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcept @Test void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 472); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -787,7 +864,7 @@ void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { @Test void initNotificationSignature_throwsSmartIdClientException() { SmartIdRestServiceStubs.stubPostErrorResponse( - "/signature/notification/etsi/PNOEE-48010010101", 480); + SIGNATURE_WITH_PERSON_CODE_PATH, 480); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -801,7 +878,7 @@ void initNotificationSignature_throwsSmartIdClientException() { @Test void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/etsi/PNOEE-48010010101", 580); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -816,6 +893,8 @@ void initNotificationSignature_throwsServerMaintenanceException() { @WireMockTest(httpPort = 18085) class DocumentNumberNotificationSignature { + private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; + private SmartIdRestConnector connector; @BeforeEach @@ -825,7 +904,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -841,7 +920,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -853,7 +932,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -865,7 +944,7 @@ void initNotificationSignature_requestIsUnauthorized_throwException() { @Test void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 471); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -877,7 +956,7 @@ void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcept @Test void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 472); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -889,7 +968,7 @@ void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { @Test void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 480); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -903,7 +982,7 @@ void initNotificationSignature_throwsSmartIdClientException() { @Test void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse("/signature/notification/document/PNOEE-48010010101-MOCK-Q", 580); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -914,7 +993,7 @@ void initNotificationSignature_throwsServerMaintenanceException() { } } - private AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); @@ -931,7 +1010,7 @@ private AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() return dynamicLinkAuthenticationSessionRequest; } - private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { + private static AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); @@ -947,7 +1026,7 @@ private AuthenticationSessionRequest toNotificationAuthenticationSessionRequest( return dynamicLinkAuthenticationSessionRequest; } - private CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { var request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); request.setRelyingPartyName("DEMO"); @@ -956,7 +1035,7 @@ private CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { return request; } - private SignatureSessionRequest createSignatureSessionRequest() { + private static SignatureSessionRequest createSignatureSessionRequest() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); request.setRelyingPartyName("BANK123"); @@ -972,7 +1051,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { return request; } - private SignatureSessionRequest createNotificationSignatureSessionRequest() { + private static SignatureSessionRequest createNotificationSignatureSessionRequest() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); request.setRelyingPartyName("DEMO"); @@ -986,4 +1065,17 @@ private SignatureSessionRequest createNotificationSignatureSessionRequest() { return request; } -} \ No newline at end of file + + private static void assertResponseValues(DynamicLinkSessionResponse response, + String expectedSessionToken, + String expectedSessionSecret, + Instant start, + Instant end) { + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + assertEquals(expectedSessionToken, response.getSessionToken()); + assertEquals(expectedSessionSecret, response.getSessionSecret()); + assertTrue(start.isBefore(response.getReceivedAt())); + assertTrue(end.isAfter(response.getReceivedAt())); + } +} diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java deleted file mode 100644 index 9587b8dc..00000000 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestIntegrationTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package ee.sk.smartid.v3.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.RandomChallenge; -import ee.sk.smartid.v3.SignatureAlgorithm; -import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; - -@Disabled("Currently request to v3 path returns - No permission to issue the request") -@SmartIdDemoIntegrationTest -class SmartIdRestIntegrationTest { - - private SmartIdConnector smartIdConnector; - - @BeforeEach - void setUp() { - smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); - } - - @Test - void authenticate_anonymous() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); - - DynamicLinkSessionResponse response = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); - } - - @Test - void authenticate_withDocumentNumber() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); - - DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-50609019996-MOCK-Q"); - } - - @Test - void authenticate_withSemanticsIdentifier() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); - - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); - - DynamicLinkSessionResponse response = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-50609019996")); - } - - private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - request.setRelyingPartyName("DEMO"); - request.setCertificateLevel("QUALIFIED"); - - String randomChallenge = RandomChallenge.generate(); - var signatureParameters = new AcspV1SignatureProtocolParameters(); - signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - signatureParameters.setRandomChallenge(randomChallenge); - request.setSignatureProtocolParameters(signatureParameters); - return request; - } -} diff --git a/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert new file mode 100644 index 00000000..e27cb966 --- /dev/null +++ b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/sign-cert-40504040001.pem.crt b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt new file mode 100644 index 00000000..e27cb966 --- /dev/null +++ b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates.jks b/src/test/resources/trusted_certificates.jks index afa683a84b52d53b2fcb12d3eb2aee265863d20c..34df03abc1ba124aac49207627d67eba673fec6e 100644 GIT binary patch delta 2119 zcmZpa*`mYq@9n?03=9lRK+Lj{XCGU=dr4|>i9&vwLUFc2W{N^_eokpgW`16=LTYA; zZeF2+k%5t6s)7bCRYm#vB?{?=shSK7j8nz`*P1Y}M(CLuSTZm$uQX_4UJAr)3z(T0 znV1Cply{eJUHu|5;fL_8$_`N(11>fWt@<{PbG9tZ%m#Ue+yAI!rLWRs{3s>OWJg2^~w)x4`e9q6rEx`m-YATNA0uj8~qLo zuxBu_u{D!B_I}HGNmC#1oPMWv`J0JxAO9V758JS~i80xri80QZu4}(Et2a^-yhI!M! z+Zf6Gy?y)G>xgcpa_?8s!kcYEEhIvY|KXY*y*Ko0(pi>?Z}J`_+zd{7YC02a5>8{{Fk204@V)Tm;nPEdxvp%nXbTjSUQq zj6g+zrGcrjA((3zWl(6SW1t0062JmQ8AtvFXJ2scHF3pJE=>+#yO-<=xXI9ue{FnyY-h)-P``lRHItk_IxQ=u&Pm%FY>3tW-(yi zZwBW5dT?GPnE%l;Gx-@6TQN|NUJevMa%84Kx&a$BN5+DM48jaTkTRwR-i%q4TAW{6 zl$=_upPX7$l9`s7oCvDqbPI|SlS?v_Q+10=5=&BZQ}gOe^f3z%;>!<2;kZFXT(aYF z+K;mu5vkR_A~zw6hxv?8qHni6>ITp#9EHB#Qk<0$h#&M!}cX~pcf^vf0 zxeIpk4|}_8SG-y)Zcu)yC29XBF{3T2Y3wTneIM`kDeXwwtanRXr6nq94zO$p+;Xx~ z?L`bzqyPQIw@qYsto|dk>_0<`QSQ<36MwfKew){GQ^s+!$|670Imzuen*;5)wQW#~ tdYiL{W9i?ehZoA!t8PBZrok@qk6SW#Pv68qfh~9Tz5KUqFQeoB Date: Tue, 8 Apr 2025 11:39:34 +0300 Subject: [PATCH 17/57] Add migration guide (#103) * SLIB-69 - add receivedAt as a read-only field to DynamicLinkSessionResponse * SLIB-69 - fix notification-based certificate choice query to ignore unknown fields; add unknown fields to v3 test responses * SLIB-69 - add integration tests for SmartIdRestConnector * SLIB-69 - change dynamicLinkAuthenticationResponse with mapper and validators to be used for notification-based authentication * SLIB-69 - add ReadmeIntegrationTest.java for authentication flows; update ReadMe * SLIB-69 - add Readme test for certificate choice; fix typos; update Readme * SLIB-69 - replace certificate in SignatureResponseMapperTest * SLIB-69 - add certificate choice response mapper * SLIB-69 - add Readme tests for signature session; update Readme; fix typos; refactoring test code * SLIB-69 - move v2 ReadmeTest to v2 test package * SLIB-69 - add license header for new files * SLIB-69 - code review fixes * SLIB-69 - update session status tests * SLIB-69 - update years in license header copyright section * SLIB-69 - update changelog * SLIB-56 - add migration guide * SLIB-56 - fix review issues * SLIB-56 - improving Readme --- MIGRATION_GUIDE.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 14 ++++----- 2 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 MIGRATION_GUIDE.md diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..e437b141 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,73 @@ +# Intro + +Library v3.0 supports Smart-ID v2 and Smart-ID v3 API. +All the previous code was moved to v2 package and all the code necessary for Smart-ID API v3 is under package v3. +Some classes could also be used in v3 and for those classes the package did not change. + +# Why was the code for Smart-ID v2 API not removed? + +To allow more gradual migration from Smart-ID v2 to Smart-ID v3, the library v3.0 supports both versions of the Smart-ID API. +For example, switching to Smart-ID v3 authentication can be done first and then switching to Smart-ID v3 signing can be done later. +Smart-ID v3 authentication already improves security and gives time to plan migration for signing with Smart-ID API v3. +At later date code for Smart-ID v2 will be marked deprecated and after some time will be removed. + +# Migration from library v2.* version to v3.0 + +To keep using Smart-ID v2 API with library v3.0 you need to update the imports in your code. +For example path from `ee.sk.smartid.SmartIdClient` to `ee.sk.smartid.v2.SmartIdClient`. This should work for most of the classes. + +# Migrating from Smart-ID v2 to Smart-ID v3 API + +For Smart-ID v3 API replace `ee.sk.smartid.v2.SmartIdClient` with `ee.sk.smartid.v3.SmartIdClient`. + +## Migrating authentication + +To migrate from Smart-ID v2 to Smart-ID v3 authentication you need to change the following: +`ee.sk.smartid.v3.SmartIdClient` provides methods `createDynamicLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. +It is recommended to start using dynamic-link authentication flows from Smart-ID API v3 as these are more secure. + +### Overview of V2 authentication flow + +1. Create authentication hash +2. Generate verification code from authentication hash +3. Verification code can be shown to the user +4. Create builder and set values. [Checkout setting values for authentication](README.md#examples-of-performing-authentication) +5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. +6. After session status is `COMPLETE` response will be checked in the build method. +7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. [Validating authentication response](README.md#validating-authentication-response) + +### Moving to V3 authentication flow + +1. Replace generating authentication hash with generating random challenge using `RandomChallenge.generate()` +2. [Create dynamic-link authentication builder and set values](README.md#examples-of-initiating-a-dynamic-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` +3. Replace showing verification code with showing dynamic link or QR-code. Recommended to use dynamic link for same device and QR-code for cross-device authentication. + - [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. +4. Querying session status can be done in parallel while displaying dynamic content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.v3.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. +5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseMapper`, that will validate required fields and will also handler errors. `AuthenticationResponse` will be returned when everything is ok. +6. Finally use `ee.sk.smartid.v3.AuthenticationResponseValidator` to validate the certificate and the signature in the response. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. + +## Migrating signing + +Before migrating please read through [session types documentation](https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/sessions.html). It provides information about what has to be considered for implementing signing flow. +In here will be focusing on [signing on same device with prior authentication session](https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.2/sessions.html#_signing_with_prior_authentication_2). + +### Overview of V2 signing flow + +1. Set values for certificate choice builder and call build method. Should return certificate as a response. +2. Use queried certificate to create DataToSign object. Requires digidoc4j library. +3. Create SignableData from DataToSign. +4. Create verification code from SignableData +5. Create signature builder and set values. [Checkout setting values for signing](README.md#create-the-signature) +6. Call build method (`sign()`) to create signing session and to start polling for session status. +7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. + +### Moving to V3 signing flow + +1. Replace certificate choice builder with`NotificationCertificateChoiceSessionRequestBuilder`. SmartID client `ee.sk.smartid.v3.SmartIdClient` provides method `createNotificationCertificateChoice()` for easier access. Call build method `.initCertificateChoice()` to start the certificate choice session. Checkout example [here](README.md#examples-of-initiating-a-notification-based-certificate-choice-session). +2. Poll for session status with `sessionStatusPoller::fetchFinalSessionState(sessionID)`. +3. If session status state is `COMPLETE` then check response with `CertificateChoiceResponseMapper` for errors and to validate required fields. `CertificateChoiceResponse` will be returned when everything is ok. +4. Replace V2 SignableData with `ee.sk.smartid.v3.SignableData`. In V3 SignableData the code to generate verification code was removed other than should be same as before. NB! If you are using Digidoc4j `DataToSign` make sure hash type in signable data matches digest algorithm in DataToSign. +5. Use `ee.sk.smartid.v3.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-dynamic-link-signature-session) `createDynamicLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +6. Replace showing verification code with showing dynamic link or QR-code. [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. +7. Poll for session status until its complete. +8. Validate session response with `SignatureSessionResponseMapper` and validate required fields. `SignatureSessionResponse` will be returned when everything is ok. \ No newline at end of file diff --git a/README.md b/README.md index b2cd25cd..df9bbd70 100644 --- a/README.md +++ b/README.md @@ -867,8 +867,8 @@ Scanning QR-code or clicking on dynamic link will prove that the certificates of #### Request Parameters -* `relyingPartyUUID`: UUID of the Relying Party. -* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. * `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. @@ -909,7 +909,7 @@ The request parameters for the dynamic-link signature session are as follows: * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. * `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. @@ -1252,7 +1252,7 @@ if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ #### Example of querying sessions status only once The following example shows how to use the SessionStatusPoller to only query the sessions status single time. -NB! If using this method for dynamic-link flows. Make sure the pollingSleepTimeout and +NB! If using this method for dynamic-link flows. Make sure the pollingSleepTimeout is not set or does not impact generating the dynamic-content for every second. ```java *SessionResponse sessionResponse; @@ -1394,7 +1394,7 @@ The session status response may return various error codes indicating the outcom * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V1. * `signatureProtocolParameters`: Required. Parameters for the ACSP_V1 signature protocol. * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. @@ -1480,8 +1480,8 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- #### Request parameters -* `relyingPartyUUID`: UUID of the Relying Party. -* `relyingPartyName`: RP friendly name, one of those configured for the particular RP. Limited to 32 bytes in UTF-8 encoding. +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. * `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. From 5133c74a45995cd4400c0a88e6bc9845a217b948 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Tue, 8 Apr 2025 12:39:42 +0300 Subject: [PATCH 18/57] Fix certificate level comparison for QUALIFIED and QSCD (#104) --- .../ee/sk/smartid/v3/CertificateLevel.java | 2 +- .../smartid/v3/SignatureResponseMapper.java | 9 +++--- .../CertificateChoiceResponseMapperTest.java | 28 +++++++++++++++-- .../v3/SignatureResponseMapperTest.java | 30 ++++++++++--------- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java index 464d3613..a9ad98b3 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/v3/CertificateLevel.java @@ -44,6 +44,6 @@ public enum CertificateLevel { * @return the level of the certificate */ public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { - return this == certificateLevel || this.level > certificateLevel.level; + return this == certificateLevel || this.level >= certificateLevel.level; } } diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java index 90fd4a6e..9d28f65f 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java @@ -154,14 +154,13 @@ private static void validateCertificate(SessionCertificate sessionCertificate, S try { X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); cert.checkValidity(); - - if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { - throw new CertificateLevelMismatchException(); - } - } catch (Exception e) { throw new SmartIdClientException("Certificate validation failed", e); } + + if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { + throw new CertificateLevelMismatchException(); + } } private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { diff --git a/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java index 08e0ff7f..a26bd6da 100644 --- a/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import ee.sk.smartid.FileUtil; @@ -75,6 +76,29 @@ void from() { assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); } + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void from_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); + sessionCertificate.setCertificateLevel("QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(toX509Certificate(), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + @Test void from_returnedCertificateHigherThanRequested_ok() { var sessionResult = new SessionResult(); diff --git a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java index b952ebe4..dce138ce 100644 --- a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; @@ -152,18 +153,8 @@ void from_certificateLevelMismatch() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); sessionStatus.getCert().setCertificateLevel("ADVANCED"); - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); - } - - @Test - void from_withQscdRequestedAndQualifiedReturned() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getCert().setCertificateLevel("QUALIFIED"); - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QSCD")); - assertTrue(ex.getCause() instanceof CertificateLevelMismatchException); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); } } @@ -179,6 +170,17 @@ void from_validRawDigestSignature() { assertEquals("OK", response.getEndResult()); } + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void from_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = SignatureResponseMapper.from(sessionStatus, certificateLevel.name()); + assertEquals("OK", response.getEndResult()); + assertEquals("QUALIFIED", response.getCertificateLevel()); + } + @Test void from_rawDigestUnexpectedAlgorithm() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); From 54d8dc0f82d690e647019079058cd55ccd27d0dc Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Tue, 27 May 2025 06:19:24 +0300 Subject: [PATCH 19/57] SLIB-91 - removed v2 package, updated documentation (#109) * Merge branch 'master' of https://github.com/ragnarhaide/smart-id-java-client into v3.1 # Conflicts: # .travis.yml # pom.xml # src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java # src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java # src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java # src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java # src/test/resources/demo_server_trusted_ssl_certs.jks # src/test/resources/demo_server_trusted_ssl_certs.p12 # src/test/resources/trusted_certificates.jks # travis.sh * SLIB-90 - updated dependency-check-maven plugin * SLIB-92 - removed v2 package, updated documentation * SLIB-91 - updated README, added missing tests, --------- Co-authored-by: ragnar.haide --- .github/workflows/check.yaml | 39 + .github/workflows/publish.yaml | 53 + .github/workflows/tests.yaml | 37 + .travis.yml | 14 - CHANGELOG.md | 7 + LICENSE.3RD-PARTY | 2 +- MIGRATION_GUIDE.md | 30 +- README.md | 707 ++-------- pom.xml | 2 +- private.key.enc | Bin 2560 -> 0 bytes publish.sh | 28 - .../java/ee/sk/smartid/{v3 => }/AuthCode.java | 2 +- .../AuthenticationCertificateLevel.java | 2 +- .../smartid/{v2 => }/AuthenticationHash.java | 5 +- .../{v3 => }/AuthenticationResponse.java | 3 +- .../AuthenticationResponseMapper.java | 11 +- .../AuthenticationResponseValidator.java | 67 +- .../{v3 => }/CertificateChoiceResponse.java | 2 +- .../CertificateChoiceResponseMapper.java | 9 +- .../sk/smartid/{v3 => }/CertificateLevel.java | 2 +- .../{v3 => }/DynamicContentBuilder.java | 2 +- ...nkAuthenticationSessionRequestBuilder.java | 14 +- ...ertificateChoiceSessionRequestBuilder.java | 10 +- ...micLinkSignatureSessionRequestBuilder.java | 16 +- .../sk/smartid/{v3 => }/DynamicLinkType.java | 2 +- .../smartid/{v3 => }/ErrorResultHandler.java | 2 +- ...onAuthenticationSessionRequestBuilder.java | 18 +- ...ertificateChoiceSessionRequestBuilder.java | 10 +- ...icationSignatureSessionRequestBuilder.java | 18 +- .../sk/smartid/{v3 => }/QrCodeGenerator.java | 2 +- .../sk/smartid/{v3 => }/RandomChallenge.java | 2 +- .../ee/sk/smartid/{v3 => }/SessionType.java | 2 +- .../ee/sk/smartid/{v3 => }/SignableData.java | 5 +- .../ee/sk/smartid/{v3 => }/SignableHash.java | 4 +- .../smartid/{v3 => }/SignatureAlgorithm.java | 2 +- .../smartid/{v3 => }/SignatureProtocol.java | 2 +- .../smartid/{v3 => }/SignatureResponse.java | 2 +- .../{v3 => }/SignatureResponseMapper.java | 11 +- .../ee/sk/smartid/{v3 => }/SignatureUtil.java | 3 +- .../ee/sk/smartid/{v3 => }/SmartIdClient.java | 8 +- .../{v2 => }/VerificationCodeCalculator.java | 5 +- .../{v3 => }/rest/SessionStatusPoller.java | 4 +- .../{v3 => }/rest/SmartIdConnector.java | 18 +- .../{v3 => }/rest/SmartIdRestConnector.java | 21 +- .../AcspV1SignatureProtocolParameters.java | 2 +- .../dao/AuthenticationSessionRequest.java | 4 +- .../dao/CertificateChoiceSessionRequest.java | 2 +- .../rest/dao/DynamicLinkInteraction.java | 6 +- .../rest/dao/DynamicLinkInteractionFlow.java | 2 +- .../rest/dao/DynamicLinkSessionResponse.java | 2 +- .../{v3 => }/rest/dao/Interaction.java | 2 +- .../{v3 => }/rest/dao/InteractionFlow.java | 2 +- ...ficationAuthenticationSessionResponse.java | 2 +- ...ationCertificateChoiceSessionResponse.java | 2 +- .../rest/dao/NotificationInteraction.java | 6 +- .../rest/dao/NotificationInteractionFlow.java | 2 +- .../NotificationSignatureSessionResponse.java | 2 +- .../RawDigestSignatureProtocolParameters.java | 2 +- .../{v3 => }/rest/dao/RequestProperties.java | 2 +- .../{v3 => }/rest/dao/SessionCertificate.java | 2 +- .../{v3 => }/rest/dao/SessionResult.java | 2 +- .../{v3 => }/rest/dao/SessionSignature.java | 2 +- .../{v3 => }/rest/dao/SessionStatus.java | 2 +- .../rest/dao/SessionStatusRequest.java | 2 +- .../rest/dao/SignatureSessionRequest.java | 4 +- .../{v3 => }/rest/dao/VerificationCode.java | 2 +- .../v2/AuthenticationRequestBuilder.java | 395 ------ .../v2/AuthenticationResponseValidator.java | 281 ---- .../ee/sk/smartid/v2/CertificateLevel.java | 60 - .../smartid/v2/CertificateRequestBuilder.java | 356 ----- .../java/ee/sk/smartid/v2/SignableData.java | 89 -- .../java/ee/sk/smartid/v2/SignableHash.java | 88 -- .../smartid/v2/SignatureRequestBuilder.java | 392 ------ .../v2/SmartIdAuthenticationResponse.java | 153 --- .../ee/sk/smartid/v2/SmartIdCertificate.java | 71 - .../java/ee/sk/smartid/v2/SmartIdClient.java | 291 ---- .../sk/smartid/v2/SmartIdRequestBuilder.java | 235 ---- .../ee/sk/smartid/v2/SmartIdSignature.java | 97 -- .../smartid/v2/rest/SessionStatusPoller.java | 87 -- .../sk/smartid/v2/rest/SmartIdConnector.java | 63 - .../smartid/v2/rest/SmartIdRestConnector.java | 327 ----- .../dao/AuthenticationSessionRequest.java | 136 -- .../dao/AuthenticationSessionResponse.java | 45 - .../ee/sk/smartid/v2/rest/dao/Capability.java | 31 - .../rest/dao/CertificateChoiceResponse.java | 45 - .../v2/rest/dao/CertificateRequest.java | 100 -- .../sk/smartid/v2/rest/dao/Interaction.java | 132 -- .../smartid/v2/rest/dao/InteractionFlow.java | 53 - .../v2/rest/dao/RequestProperties.java | 52 - .../v2/rest/dao/SessionCertificate.java | 54 - .../sk/smartid/v2/rest/dao/SessionResult.java | 54 - .../smartid/v2/rest/dao/SessionSignature.java | 55 - .../sk/smartid/v2/rest/dao/SessionStatus.java | 113 -- .../v2/rest/dao/SessionStatusRequest.java | 73 - .../v2/rest/dao/SignatureSessionRequest.java | 136 -- .../v2/rest/dao/SignatureSessionResponse.java | 45 - .../trusted_certificates/EID_NQ_2021E.der.crt | Bin 0 -> 912 bytes .../trusted_certificates/EID_NQ_2021R.der.crt | Bin 0 -> 1722 bytes .../trusted_certificates/EID_Q_2024E.der.crt | Bin 0 -> 947 bytes .../trusted_certificates/EID_Q_2024R.der.crt | Bin 0 -> 1757 bytes .../ee/sk/smartid/{v3 => }/AuthCodeTest.java | 2 +- .../{v2 => }/AuthenticationIdentityTest.java | 4 +- .../AuthenticationResponseMapperTest.java | 11 +- .../AuthenticationResponseValidatorTest.java | 37 +- .../CertificateChoiceResponseMapperTest.java | 9 +- .../sk/smartid/{v2 => }/CertificateUtil.java | 4 +- .../{v2 => }/ClientRequestHeaderFilter.java | 2 +- .../{v3 => }/DynamicContentBuilderTest.java | 2 +- ...thenticationSessionRequestBuilderTest.java | 10 +- ...ficateChoiceSessionRequestBuilderTest.java | 8 +- ...inkSignatureSessionRequestBuilderTest.java | 11 +- .../{v3 => }/ErrorResultHandlerTest.java | 2 +- ...thenticationSessionRequestBuilderTest.java | 12 +- ...ficateChoiceSessionRequestBuilderTest.java | 8 +- ...ionSignatureSessionRequestBuilderTest.java | 13 +- .../smartid/{v3 => }/QrCodeGeneratorTest.java | 2 +- .../ee/sk/smartid/{v3 => }/QrCodeUtil.java | 2 +- .../smartid/{v3 => }/RandomChallengeTest.java | 2 +- ...essionEndResultErrorArgumentsProvider.java | 2 +- .../java/ee/sk/smartid/SignableDataTest.java | 72 - .../{v3 => }/SignatureResponseMapperTest.java | 11 +- .../smartid/{v3 => }/SignatureUtilTest.java | 3 +- .../smartid/{v3 => }/SmartIdClientTest.java | 53 +- .../VerificationCodeCalculatorTest.java | 5 +- .../dao/SemanticsIdentifierTest.java | 2 +- .../integration/ReadmeIntegrationTest.java | 48 +- .../SmartIdRestIntegrationTest.java | 34 +- .../rest/SessionStatusPollerTest.java | 6 +- .../rest/SmartIdRestConnectorTest.java | 120 +- .../util/CertificateAttributeUtilTest.java | 5 +- .../util/NationalIdentityNumberUtilTest.java | 13 +- .../v2/AuthenticationRequestBuilderTest.java | 628 --------- .../AuthenticationResponseValidatorTest.java | 359 ----- .../sk/smartid/v2/CertificateLevelTest.java | 79 -- .../v2/CertificateRequestBuilderTest.java | 341 ----- src/test/java/ee/sk/smartid/v2/DummyData.java | 65 - ...ndpointSslVerificationIntegrationTest.java | 289 ---- .../ee/sk/smartid/v2/SignableHashTest.java | 53 - .../v2/SignatureRequestBuilderTest.java | 543 -------- .../v2/SmartIdAuthenticationResponseTest.java | 73 - .../ee/sk/smartid/v2/SmartIdClientTest.java | 1191 ----------------- .../sk/smartid/v2/SmartIdSignatureTest.java | 61 - .../sk/smartid/v2/integration/ReadmeTest.java | 706 ---------- .../integration/SmartIdIntegrationTest.java | 268 ---- .../v2/rest/SessionStatusPollerTest.java | 219 --- .../smartid/v2/rest/SmartIdConnectorSpy.java | 115 -- .../v2/rest/SmartIdRestConnectorTest.java | 746 ----------- .../v2/rest/SmartIdRestIntegrationTest.java | 285 ---- .../rest/dao/SignatureSessionRequestTest.java | 43 - .../demo_server_trusted_ssl_certs.p12 | Bin 6202 -> 0 bytes .../certificate-choice-session-request.json | 0 ...c-link-authentication-session-request.json | 0 .../dynamic-link-signature-request.json | 0 ...cation-authentication-session-request.json | 0 ...otification-signature-session-request.json | 0 ...-link-authentication-session-response.json | 0 ...k-certificate-choice-session-response.json | 0 ...namic-link-signature-session-response.json | 0 ...n-certificate-choice-session-response.json | 0 .../notification-session-response.json | 0 .../session-status-document-unusable.json | 0 ...tatus-running-with-ignored-properties.json | 0 .../responses/session-status-running.json | 0 ...sion-status-successful-authentication.json | 0 ...-status-successful-certificate-choice.json | 0 .../session-status-successful-signature.json | 0 .../responses/session-status-timeout.json | 0 ...ssion-status-user-refused-cert-choice.json | 0 ...s-user-refused-confirmation-vc-choice.json | 0 ...sion-status-user-refused-confirmation.json | 0 ...tus-user-refused-display-text-and-pin.json | 0 ...session-status-user-refused-vc-choice.json | 0 .../session-status-user-refused.json | 0 .../responses/session-status-wrong-vc.json | 0 src/test/resources/sid_live_sk_ee.pem | 38 - src/test/resources/trusted_certificates.jks | Bin 5684 -> 0 bytes .../TEST_of_EID-SK_2016.pem.crt | 40 - .../TEST_of_NQ-SK_2016.pem.crt | 37 - .../trusted_certificates/wrong.pem.cer | 37 - .../authenticationSessionRequest.json | 11 - ...authenticationSessionRequestWithNonce.json | 12 - ...onRequestWithSingleAllowedInteraction.json | 13 - .../v2/requests/certificateChoiceRequest.json | 5 - .../certificateChoiceRequestWithNonce.json | 6 - .../v2/requests/signatureSessionRequest.json | 11 - .../signatureSessionRequestWithNonce.json | 12 - ...reSessionRequestWithRequestProperties.json | 9 - .../signatureSessionRequestWithSha512.json | 11 - ...ice_fallbackTo_verificationCodeChoice.json | 17 - ...nMessage_fallbackTo_displayTextAndPIN.json | 17 - ...age_fallbackTo_verificationCodeChoice.json | 17 - ...equest_confirmationMessage_noFallback.json | 13 - ...deChoice_fallbackTo_displayTextAndPIN.json | 17 - .../authenticationSessionResponse.json | 4 - .../responses/certificateChoiceResponse.json | 5 - ...tusForSuccessfulAuthenticationRequest.json | 17 - ...henticationRequestWithDeviceIpAddress.json | 18 - ...StatusForSuccessfulCertificateRequest.json | 12 - ...sionStatusForSuccessfulSigningRequest.json | 13 - ...sfulSigningRequestWithDeviceIpAddress.json | 14 - .../v2/responses/sessionStatusRunning.json | 5 - ...ionStatusRunningWithIgnoredProperties.json | 6 - .../sessionStatusWhenDocumentUnusable.json | 7 - ...nRequiredInteractionNotSupportedByApp.json | 7 - .../responses/sessionStatusWhenTimeout.json | 7 - .../sessionStatusWhenUnknownErrorCode.json | 7 - ...nStatusWhenUserHasSelectedWrongVcCode.json | 7 - ...essionStatusWhenUserRefusedCertChoice.json | 7 - ...tusWhenUserRefusedConfirmationMessage.json | 7 - ...tionMessageWithVerificationCodeChoice.json | 7 - ...tatusWhenUserRefusedDisplayTextAndPin.json | 7 - .../sessionStatusWhenUserRefusedGeneral.json | 7 - ...WhenUserRefusedVerificationCodeChoice.json | 7 - .../responses/signatureSessionResponse.json | 4 - src/test/resources/wrong_ssl_cert.jks | Bin 1725 -> 0 bytes travis.sh | 19 - 216 files changed, 675 insertions(+), 11791 deletions(-) create mode 100644 .github/workflows/check.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/tests.yaml delete mode 100644 .travis.yml delete mode 100644 private.key.enc delete mode 100755 publish.sh rename src/main/java/ee/sk/smartid/{v3 => }/AuthCode.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/AuthenticationCertificateLevel.java (98%) rename src/main/java/ee/sk/smartid/{v2 => }/AuthenticationHash.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/AuthenticationResponse.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/AuthenticationResponseMapper.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/AuthenticationResponseValidator.java (79%) rename src/main/java/ee/sk/smartid/{v3 => }/CertificateChoiceResponse.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/CertificateChoiceResponseMapper.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/CertificateLevel.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/DynamicContentBuilder.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/DynamicLinkAuthenticationSessionRequestBuilder.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/DynamicLinkCertificateChoiceSessionRequestBuilder.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/DynamicLinkSignatureSessionRequestBuilder.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/DynamicLinkType.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/ErrorResultHandler.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/NotificationAuthenticationSessionRequestBuilder.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/NotificationCertificateChoiceSessionRequestBuilder.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/NotificationSignatureSessionRequestBuilder.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/QrCodeGenerator.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/RandomChallenge.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/SessionType.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/SignableData.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/SignableHash.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/SignatureAlgorithm.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/SignatureProtocol.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/SignatureResponse.java (99%) rename src/main/java/ee/sk/smartid/{v3 => }/SignatureResponseMapper.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/SignatureUtil.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/SmartIdClient.java (98%) rename src/main/java/ee/sk/smartid/{v2 => }/VerificationCodeCalculator.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/SessionStatusPoller.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/SmartIdConnector.java (93%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/SmartIdRestConnector.java (96%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/AcspV1SignatureProtocolParameters.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/AuthenticationSessionRequest.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/CertificateChoiceSessionRequest.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/DynamicLinkInteraction.java (91%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/DynamicLinkInteractionFlow.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/DynamicLinkSessionResponse.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/Interaction.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/InteractionFlow.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/NotificationAuthenticationSessionResponse.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/NotificationCertificateChoiceSessionResponse.java (97%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/NotificationInteraction.java (90%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/NotificationInteractionFlow.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/NotificationSignatureSessionResponse.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/RawDigestSignatureProtocolParameters.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/RequestProperties.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SessionCertificate.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SessionResult.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SessionSignature.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SessionStatus.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SessionStatusRequest.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/SignatureSessionRequest.java (98%) rename src/main/java/ee/sk/smartid/{v3 => }/rest/dao/VerificationCode.java (98%) delete mode 100644 src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java delete mode 100644 src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java delete mode 100644 src/main/java/ee/sk/smartid/v2/CertificateLevel.java delete mode 100644 src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SignableData.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SignableHash.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SmartIdClient.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java delete mode 100644 src/main/java/ee/sk/smartid/v2/SmartIdSignature.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java create mode 100644 src/main/resources/trusted_certificates/EID_NQ_2021E.der.crt create mode 100644 src/main/resources/trusted_certificates/EID_NQ_2021R.der.crt create mode 100644 src/main/resources/trusted_certificates/EID_Q_2024E.der.crt create mode 100644 src/main/resources/trusted_certificates/EID_Q_2024R.der.crt rename src/test/java/ee/sk/smartid/{v3 => }/AuthCodeTest.java (99%) rename src/test/java/ee/sk/smartid/{v2 => }/AuthenticationIdentityTest.java (96%) rename src/test/java/ee/sk/smartid/{v3 => }/AuthenticationResponseMapperTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/AuthenticationResponseValidatorTest.java (89%) rename src/test/java/ee/sk/smartid/{v3 => }/CertificateChoiceResponseMapperTest.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/CertificateUtil.java (97%) rename src/test/java/ee/sk/smartid/{v2 => }/ClientRequestHeaderFilter.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/DynamicContentBuilderTest.java (99%) rename src/test/java/ee/sk/smartid/{v3 => }/DynamicLinkAuthenticationSessionRequestBuilderTest.java (99%) rename src/test/java/ee/sk/smartid/{v3 => }/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java (97%) rename src/test/java/ee/sk/smartid/{v3 => }/DynamicLinkSignatureSessionRequestBuilderTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/ErrorResultHandlerTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/NotificationAuthenticationSessionRequestBuilderTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/NotificationCertificateChoiceSessionRequestBuilderTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/NotificationSignatureSessionRequestBuilderTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/QrCodeGeneratorTest.java (99%) rename src/test/java/ee/sk/smartid/{v3 => }/QrCodeUtil.java (99%) rename src/test/java/ee/sk/smartid/{v3 => }/RandomChallengeTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/SessionEndResultErrorArgumentsProvider.java (99%) delete mode 100644 src/test/java/ee/sk/smartid/SignableDataTest.java rename src/test/java/ee/sk/smartid/{v3 => }/SignatureResponseMapperTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/SignatureUtilTest.java (99%) rename src/test/java/ee/sk/smartid/{v3 => }/SmartIdClientTest.java (85%) rename src/test/java/ee/sk/smartid/{v2 => }/VerificationCodeCalculatorTest.java (96%) rename src/test/java/ee/sk/smartid/{v2/rest => }/dao/SemanticsIdentifierTest.java (98%) rename src/test/java/ee/sk/smartid/{v3 => }/integration/ReadmeIntegrationTest.java (97%) rename src/test/java/ee/sk/smartid/{v3 => }/integration/SmartIdRestIntegrationTest.java (94%) rename src/test/java/ee/sk/smartid/{v3 => }/rest/SessionStatusPollerTest.java (94%) rename src/test/java/ee/sk/smartid/{v3 => }/rest/SmartIdRestConnectorTest.java (90%) rename src/test/java/ee/sk/smartid/{v2 => }/util/CertificateAttributeUtilTest.java (98%) rename src/test/java/ee/sk/smartid/{v2 => }/util/NationalIdentityNumberUtilTest.java (96%) delete mode 100644 src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/DummyData.java delete mode 100644 src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/SignableHashTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java delete mode 100644 src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java delete mode 100644 src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java delete mode 100644 src/test/resources/demo_server_trusted_ssl_certs.p12 rename src/test/resources/{v3 => }/requests/certificate-choice-session-request.json (100%) rename src/test/resources/{v3 => }/requests/dynamic-link-authentication-session-request.json (100%) rename src/test/resources/{v3 => }/requests/dynamic-link-signature-request.json (100%) rename src/test/resources/{v3 => }/requests/notification-authentication-session-request.json (100%) rename src/test/resources/{v3 => }/requests/notification-signature-session-request.json (100%) rename src/test/resources/{v3 => }/responses/dynamic-link-authentication-session-response.json (100%) rename src/test/resources/{v3 => }/responses/dynamic-link-certificate-choice-session-response.json (100%) rename src/test/resources/{v3 => }/responses/dynamic-link-signature-session-response.json (100%) rename src/test/resources/{v3 => }/responses/notification-certificate-choice-session-response.json (100%) rename src/test/resources/{v3 => }/responses/notification-session-response.json (100%) rename src/test/resources/{v3 => }/responses/session-status-document-unusable.json (100%) rename src/test/resources/{v3 => }/responses/session-status-running-with-ignored-properties.json (100%) rename src/test/resources/{v3 => }/responses/session-status-running.json (100%) rename src/test/resources/{v3 => }/responses/session-status-successful-authentication.json (100%) rename src/test/resources/{v3 => }/responses/session-status-successful-certificate-choice.json (100%) rename src/test/resources/{v3 => }/responses/session-status-successful-signature.json (100%) rename src/test/resources/{v3 => }/responses/session-status-timeout.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused-cert-choice.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused-confirmation-vc-choice.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused-confirmation.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused-display-text-and-pin.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused-vc-choice.json (100%) rename src/test/resources/{v3 => }/responses/session-status-user-refused.json (100%) rename src/test/resources/{v3 => }/responses/session-status-wrong-vc.json (100%) delete mode 100644 src/test/resources/sid_live_sk_ee.pem delete mode 100644 src/test/resources/trusted_certificates.jks delete mode 100644 src/test/resources/trusted_certificates/TEST_of_EID-SK_2016.pem.crt delete mode 100644 src/test/resources/trusted_certificates/TEST_of_NQ-SK_2016.pem.crt delete mode 100644 src/test/resources/trusted_certificates/wrong.pem.cer delete mode 100644 src/test/resources/v2/requests/authenticationSessionRequest.json delete mode 100644 src/test/resources/v2/requests/authenticationSessionRequestWithNonce.json delete mode 100644 src/test/resources/v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json delete mode 100644 src/test/resources/v2/requests/certificateChoiceRequest.json delete mode 100644 src/test/resources/v2/requests/certificateChoiceRequestWithNonce.json delete mode 100644 src/test/resources/v2/requests/signatureSessionRequest.json delete mode 100644 src/test/resources/v2/requests/signatureSessionRequestWithNonce.json delete mode 100644 src/test/resources/v2/requests/signatureSessionRequestWithRequestProperties.json delete mode 100644 src/test/resources/v2/requests/signatureSessionRequestWithSha512.json delete mode 100644 src/test/resources/v2/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json delete mode 100644 src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json delete mode 100644 src/test/resources/v2/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json delete mode 100644 src/test/resources/v2/requests/signingRequest_confirmationMessage_noFallback.json delete mode 100644 src/test/resources/v2/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json delete mode 100644 src/test/resources/v2/responses/authenticationSessionResponse.json delete mode 100644 src/test/resources/v2/responses/certificateChoiceResponse.json delete mode 100644 src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json delete mode 100644 src/test/resources/v2/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json delete mode 100644 src/test/resources/v2/responses/sessionStatusForSuccessfulCertificateRequest.json delete mode 100644 src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequest.json delete mode 100644 src/test/resources/v2/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json delete mode 100644 src/test/resources/v2/responses/sessionStatusRunning.json delete mode 100644 src/test/resources/v2/responses/sessionStatusRunningWithIgnoredProperties.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenDocumentUnusable.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenTimeout.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUnknownErrorCode.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedCertChoice.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedGeneral.json delete mode 100644 src/test/resources/v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json delete mode 100644 src/test/resources/v2/responses/signatureSessionResponse.json delete mode 100644 src/test/resources/wrong_ssl_cert.jks delete mode 100644 travis.sh diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 00000000..31eff898 --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,39 @@ +name: Run dependency and spotbugs checks + +on: + workflow_run: + workflows: ["Run tests"] + types: + - completed + +permissions: + contents: read + +jobs: + run-checks: + name: Run dependency and spotbugs checks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + cache: maven + + - name: Run dependency check + run: | + ./mvnw org.owasp:dependency-check-maven:check + + - name: Archive dependency report + uses: actions/upload-artifact@v4 + with: + name: dependency-report + path: target/dependency-check-report.html + + - name: Run spotbugs check + run: | + ./mvnw spotbugs:check \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..0c830d90 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,53 @@ +name: Publish to maven repository + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + package_and_publish: + name: Publish to maven repository + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup java SDK 8 + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + cache: maven + - + name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + #passphrase: ${{ secrets.PASSPHRASE }} + + - name: Create bundle and upload to oss.sonatype.org (staging) + # Fail on first error + run: | + set -e + version=${{ github.event.release.name }} + artifact=smart-id-java-client-$version + echo "[INFO] Artifact name: $artifact" + ./mvnw versions:set -DnewVersion="$version" + ./mvnw package -DskipTests + gpg -ab pom.xml + cd target + gpg -ab $artifact.jar + gpg -ab $artifact-sources.jar + gpg -ab $artifact-javadoc.jar + jar -cvf bundle.jar ../pom.xml ../pom.xml.asc $artifact.jar $artifact.jar.asc $artifact-javadoc.jar $artifact-javadoc.jar.asc $artifact-sources.jar $artifact-sources.jar.asc + CODE=$(curl -w "%{http_code}" -o curl_response.txt -s -ujorlina2 -u ${{ secrets.SONATYPEUN }}:${{ secrets.SONATYPEPW }} --request POST -F "file=@bundle.jar" "https://oss.sonatype.org/service/local/staging/bundle_upload") + echo "[INFO] ------------------------------------------------------------------------" + echo "[INFO] Upload to oss.sonatype.org ResponseCode: $CODE" + cat curl_response.txt + echo -e "\n[INFO] Login to oss.sonatype.org for releasing $artifact" + echo "[INFO] ------------------------------------------------------------------------" + [[ $CODE == 201 ]] && exit 0 || exit 1 + \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..e870b229 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,37 @@ +name: Run tests + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: ['8', '11', '17', '21'] + name: Run tests with java SDK ${{ matrix.java-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + + - name: Check JAVA version (v${{ matrix.java-version }}) + run: java -version + + - name: Run tests + # Fail on first error + run: | + set -e + mvn test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d3aba720..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: java -jdk: -- openjdk17 -sudo: required -env: - global: - - secure: Ba3gi94kSz/kCDI6kYYRP8zSiO/rOr8nIPcPhfKETpFxbD1wb/EQP9sSRptZ1Aj4hmYio/rh7RfcH5aX56QUMxOZ/MLBzkfBkGcjdI+CusJwfHOBN/Yufqw2r6SK09lmEdAGM4Y+GQWruBUk3cRLt9eCgA/nf02mKyAwLPOZxBfjUMvgyKWS+jeoZwWG7B0+ohD36/DoDp5PI2cxnV1RqzVNpsOY7ERjcUocoZKBc4ATpObRyoOlGpJroBDaw88i37vI1WFd0GKZmX8Iq1HhnaecwX/edXjVI6dZg/j4mVzGVvDhQTH/RljT0dDXpup02hgi2m3IS48QbLThIm3AfsGD9y37lxkpOrWL6qQG4XNOI44k5gOqMInnaDTNuWmzXfZ8/+qggQp7OE8CqrufOL1FbroNN3C9QukkRLBwzHmHL3DCDTisxZjBZRtx/L/WtFn048JukcerBislaWJrf1r7vmDpgVtQjo/JEmmru0kpsky4jC9og2wOgIn6z9+lbtksZQlNBBngp/1u5/z4o8uW82W8qZ/aKSVBqT8ZJmNING59PBOOFVwIt9WfE7NkwavfYknhSI08G9k7xzzMZi3c+g248jf1sKLVf5qq3hCfGwWMYUXljUfB+uTPLNaRxvOXLIoCF2wofYGg36Zt4EN+iq9JQrTz7qQxJh7md5A= - - secure: Y6kBvTZHREqGqHc+kmt4e5VS+1tUmNa5FuslKxDMEXZfeez4oriz0EV3u9dB3M2prV6mAUvjLWo/4DFfibuFUD+yjD1rzF7vmadV9Uw815k5+/P7NqwFAn97dGiLivWtt4miR4jBdBK7+TTCaiJfGaqc1RYb7uQOaUn4jpIfIoMPwprIRLL6LwSHMiq5z8m+9XEYr0/UJ0Ufo6vOD5PC8SOisVMuBUhNXMNQxkU0cVwDmCXFn1M/f2G19ADrN7bzIuu1hGOsqJGBBIQ+DId27amwuobs6oj0qjoUqbgWmZPSlnx0CDRFOra1C+Glu8bRKliy4rkqGI5o6ncCma+xA7FAoKsu+Q4P8xi5bUoti4qz/VXGglKSM2y9M3+o3QNPWNIXjSZ1tz5kZTjz7Nxq8UNb43tRxD2OshezHRBEsMgIePt5L5N4oaV+wIePOpeKiEsMwK+v1qNf99aNEg1cooJMPNrwwJyrX1Ff+p9gwcUwMr3RXfczjw53QU2Y3X2NSUGVyORfN4FhQ8VD7263kCtyULwVuZNxcwEH48GTiWX4DtMTTqoZDNURlgZ697yZtj5nT36SBJss/bOwKoQ7jRBJ3CHDhd+Bq8UWspjaE5D+kTk+Gaa2z4YONtLyfoD5cX18ierDckkErtv1mT0QOUUBX5mRj3RtlihwLjP9rzA= -before_install: -- eval $(openssl aes-256-cbc -K $encrypted_key -iv $encrypted_iv -in private.key.enc -out private.key -d) -- chmod +x travis.sh -install: true -script: -- "./travis.sh" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebfcbbb..2eeb28db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1] - 2025-05-20 + +### Changed +- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. +- Removed all Smart-ID v2 related classes, tests, and documentation. +- Updated README to reflect removal of v2-related information. + ## [3.0] - 2023-10-14 ### Added diff --git a/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index 83a6984f..4e788d46 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,4 +1,4 @@ -List of 112 third-party dependencies (auto-generated on 2024-11-04 with License Maven Plugin): +List of 112 third-party dependencies (auto-generated on 2025-05-19 with License Maven Plugin): * (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) * (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index e437b141..2309fcaa 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,29 +1,17 @@ # Intro -Library v3.0 supports Smart-ID v2 and Smart-ID v3 API. -All the previous code was moved to v2 package and all the code necessary for Smart-ID API v3 is under package v3. +Library v3.1 supports only Smart-ID v3 API. +All the previous v2 related code has been removed and all the code necessary for Smart-ID API v3 is under package smartid. Some classes could also be used in v3 and for those classes the package did not change. -# Why was the code for Smart-ID v2 API not removed? - -To allow more gradual migration from Smart-ID v2 to Smart-ID v3, the library v3.0 supports both versions of the Smart-ID API. -For example, switching to Smart-ID v3 authentication can be done first and then switching to Smart-ID v3 signing can be done later. -Smart-ID v3 authentication already improves security and gives time to plan migration for signing with Smart-ID API v3. -At later date code for Smart-ID v2 will be marked deprecated and after some time will be removed. - -# Migration from library v2.* version to v3.0 - -To keep using Smart-ID v2 API with library v3.0 you need to update the imports in your code. -For example path from `ee.sk.smartid.SmartIdClient` to `ee.sk.smartid.v2.SmartIdClient`. This should work for most of the classes. - # Migrating from Smart-ID v2 to Smart-ID v3 API -For Smart-ID v3 API replace `ee.sk.smartid.v2.SmartIdClient` with `ee.sk.smartid.v3.SmartIdClient`. +For Smart-ID v3 API replace `ee.sk.smartid.v2.SmartIdClient` with `ee.sk.smartid.SmartIdClient`. ## Migrating authentication To migrate from Smart-ID v2 to Smart-ID v3 authentication you need to change the following: -`ee.sk.smartid.v3.SmartIdClient` provides methods `createDynamicLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. +`ee.sk.smartid.SmartIdClient` provides methods `createDynamicLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. It is recommended to start using dynamic-link authentication flows from Smart-ID API v3 as these are more secure. ### Overview of V2 authentication flow @@ -42,9 +30,9 @@ It is recommended to start using dynamic-link authentication flows from Smart-ID 2. [Create dynamic-link authentication builder and set values](README.md#examples-of-initiating-a-dynamic-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` 3. Replace showing verification code with showing dynamic link or QR-code. Recommended to use dynamic link for same device and QR-code for cross-device authentication. - [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. -4. Querying session status can be done in parallel while displaying dynamic content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.v3.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. +4. Querying session status can be done in parallel while displaying dynamic content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. 5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseMapper`, that will validate required fields and will also handler errors. `AuthenticationResponse` will be returned when everything is ok. -6. Finally use `ee.sk.smartid.v3.AuthenticationResponseValidator` to validate the certificate and the signature in the response. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. +6. Finally use `ee.sk.smartid.AuthenticationResponseValidator` to validate the certificate and the signature in the response. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. ## Migrating signing @@ -63,11 +51,11 @@ In here will be focusing on [signing on same device with prior authentication se ### Moving to V3 signing flow -1. Replace certificate choice builder with`NotificationCertificateChoiceSessionRequestBuilder`. SmartID client `ee.sk.smartid.v3.SmartIdClient` provides method `createNotificationCertificateChoice()` for easier access. Call build method `.initCertificateChoice()` to start the certificate choice session. Checkout example [here](README.md#examples-of-initiating-a-notification-based-certificate-choice-session). +1. Replace certificate choice builder with`NotificationCertificateChoiceSessionRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createNotificationCertificateChoice()` for easier access. Call build method `.initCertificateChoice()` to start the certificate choice session. Checkout example [here](README.md#examples-of-initiating-a-notification-based-certificate-choice-session). 2. Poll for session status with `sessionStatusPoller::fetchFinalSessionState(sessionID)`. 3. If session status state is `COMPLETE` then check response with `CertificateChoiceResponseMapper` for errors and to validate required fields. `CertificateChoiceResponse` will be returned when everything is ok. -4. Replace V2 SignableData with `ee.sk.smartid.v3.SignableData`. In V3 SignableData the code to generate verification code was removed other than should be same as before. NB! If you are using Digidoc4j `DataToSign` make sure hash type in signable data matches digest algorithm in DataToSign. -5. Use `ee.sk.smartid.v3.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-dynamic-link-signature-session) `createDynamicLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace V2 SignableData with `ee.sk.smartid.SignableData`. In V3 SignableData the code to generate verification code was removed other than should be same as before. NB! If you are using Digidoc4j `DataToSign` make sure hash type in signable data matches digest algorithm in DataToSign. +5. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-dynamic-link-signature-session) `createDynamicLinkSignature()` and call build method `initSignatureSession()` to start the signing session. 6. Replace showing verification code with showing dynamic link or QR-code. [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. 7. Poll for session status until its complete. 8. Validate session response with `SignatureSessionResponseMapper` and validate required fields. `SignatureSessionResponse` will be returned when everything is ok. \ No newline at end of file diff --git a/README.md b/README.md index df9bbd70..ba4b3081 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.com/SK-EID/smart-id-java-client.svg?branch=master)](https://travis-ci.com/SK-EID/smart-id-java-client) +[![Build Status](https://app.travis-ci.com/SK-EID/smart-id-java-client.svg?branch=master)](https://app.travis-ci.com/SK-EID/smart-id-java-client) [![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) [![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) @@ -6,7 +6,7 @@ # Smart-ID Java client -This library now supports both Smart-ID API v2.0 and v3.0. +This library supports Smart-ID API v3.1. # Table of contents @@ -16,37 +16,11 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Requirements](#requirements) * [Getting the library](#getting-the-library) * [Changelog](#changelog) -* [How to use it with RP API v2.0](#how-to-use-api-v20) - * [Test accounts for testing]() +* [How to use it with RP API v3.1](#how-to-use-api-v31) + * [Test accounts for testing](#test-accounts-for-testing) * [Logging](#logging) * [Log request payloads](#log-request-payloads) - * [Get the IP address of user's device](#get-the-ip-address-of-users-device) - * [Example of configuring the client](#example-of-configuring-the-client) - * [Reading trusted certificates from key store](#reading-trusted-certificates-from-key-store) - * [Feeding trusted certificates one by one](#feeding-trusted-certificates-one-by-one) - * [Examples of performing authentication](#examples-of-performing-authentication) - * [Authenticating with semantics identifier](#authenticating-with-semantics-identifier) - * [Authenticating with document number](#authenticating-with-document-number) - * [Validating authentication response](#validating-authentication-response) - * [Extracting date-of-birth](#extracting-date-of-birth) - * [Creating a signature](#creating-a-signature) - * [Obtaining signer's certificate](#obtaining-signers-certificate) - * [Create the signature](#create-the-signature) - * [Setting the order of preferred interactions for displaying text and asking PIN](#setting-the-order-of-preferred-interactions-for-displaying-text-and-asking-pin) - * [Parameter allowedInteractionsOrder most common examples](#parameter-allowedinteractionsorder-most-common-examples) - * [Short confirmation message with PIN](#short-confirmation-message-with-pin) - * [Verification code choice](#verification-code-choice) - * [Long confirmation message with fallback to PIN](#long-confirmation-message-with-fallback-to-pin) - * [Long confirmation message together with verification code choice with fallback to verification code choice](#long-confirmation-message-together-with-verification-code-choice-with-fallback-to-verification-code-choice) - * [Interactions with longer text without fallback](#interactions-with-longer-text-without-fallback) - * [Handling exceptions](#handling-exceptions) - * [Network connection configuration of the client](#network-connection-configuration-of-the-client) - * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) - * [Configuring a proxy](#configuring-a-proxy) - * [Configuring a proxy using JBoss Resteasy library](#configuring-a-proxy-using-jboss-resteasy-library) - * [Configuring a proxy using Jersey](#configuring-a-proxy-using-jersey) -* [How to use it with RP API v3.0](#how-to-use-api-v30) - * [Setting up SmartIdClient for v3.0](#setting-up-smartidclient-for-v30) + * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) * [Dynamic link flows](#dynamic-link-flows) * [Dynamic link authentication session](#dynamic-link-authentication-session) * [Examples of authentication session](#examples-of-initiating-a-dynamic-link-authentication-session) @@ -69,9 +43,9 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Generating QR-code](#generating-qr-code) * [Generate QR-code Data URI](#generate-qr-code-data-uri) * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) - * [Querying sessions status](#session-status-request-handling-for-v30) + * [Querying sessions status](#session-status-request-handling-for-v31) * [Sessions status response](#session-status-response) - * [Example of querying session status in v3.0](#examples-of-querying-session-status-in-v30) + * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) * [Validating sessions status response](#validating-session-status-response) @@ -95,11 +69,13 @@ This library now supports both Smart-ID API v2.0 and v3.0. * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) * [Examples of allowed notification-based interactions order](#examples-of-allowed-notification-based-interactions-order) * [Exception handling](#exception-handling) + * [Network connection configuration of the client](#network-connection-configuration-of-the-client) + * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) ## Introduction The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. -This library now supports both Smart-ID API v2.0 and v3.0. The existing code for API v2.0 has been moved to the ee.sk.smartid.v2 package, and support for API v3.0 has been added in the ee.sk.smartid.v3 package. +This library supports Smart-ID API v3.1. ## Features @@ -131,7 +107,19 @@ You can use the library as a Maven dependency from the [Maven Central](https://s Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) -# How to use API v2.0 +# How to use API v3.1 + +Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. +This version introduces new dynamic link and notification-based flows for authentication, certificate choice and signing. + +NB! v2 API classes are removed. + +To use the v3.1 API, import the relevant classes from the ee.sk.smartid package. + +```java + +import ee.sk.smartid.SmartIdConnector; +``` ## Test accounts for testing @@ -148,572 +136,30 @@ For applications on Spring Boot this can be done by adding following line to app logging.level.ee.sk.smartid.rest.LoggingFilter: trace ``` -### Get the IP address of user's device - -Smart-ID API returns the IP address of the user's device for subscribed Relying Parties who -ask it to be returned. - -Requesting for the IP address to be returned: - -* [AuthenticationRequestBuilder.withShareMdClientIpAddress()](src/main/java/ee/sk/smartid/AuthenticationRequestBuilder.java) -> withShareMdClientIpAddress() -* [SignatureRequestBuilder.withShareMdClientIpAddress()](src/main/java/ee/sk/smartid/SignatureRequestBuilder.java) -> withShareMdClientIpAddress() -* [CertificateRequestBuilder.withShareMdClientIpAddress()](src/main/java/ee/sk/smartid/CertificateRequestBuilder.java) -> withShareMdClientIpAddress() - - -The returned info can be retrieved using one of: - -* [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java) -> getDeviceIpAddress() -* [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java) -> getDeviceIpAddress() -* [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java) -> getDeviceIpAddress() - - -## Example of configuring the client - -You need a client for any call to API. - -The production environment host URL, Relying Party UUID and name are fixed in the Smart-ID service agreement. - -### Verifying the SSL connection to Application Provider (SK) - -Relying Party needs to verify that it is connecting to Smart-ID API it trusts. -More info about this requirement can be found from [Smart-ID Documentation](https://github.com/SK-EID/smart-id-documentation#35-api-endpoint-authentication). - - -#### Reading trusted certificates from key store - -It is recommended to keep trusted certificates in a trust store file: - - -```java -// reading trusted certificates from external trustStore file -InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); - -// Client setup. Note that these values are demo environment specific. -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); -client.setTrustStore(trustStore); -``` - -### Feeding trusted certificates one by one - -It is also possible to feed trusted certificates one by one. -This can prove useful when trusted certificates are kept as application configuration property. - -```java -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); -client.setTrustedCertificates( - "-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj...", - "-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQbQr/Ky22GFhYWS3oQoJkyT..." -); -``` - - -## Examples of performing authentication - -### Authenticating with semantics identifier - -More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "30303039914"); // identifier (according to country and identity type reference) - -// For security reasons a new hash value must be created for each new authentication request -AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - -String verificationCode = authenticationHash.calculateVerificationCode(); - -// NB! Display verification code to the customer for a few seconds before starting next step: - -SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) - .authenticate(); - -// You need this later to pull user's signing certificate -String documentNumberForFurtherReference = authenticationResponse.getDocumentNumber(); - -// We get IP of Smart-ID app since we made the request .withShareMdClientIpAddress(true) -String deviceIpAddress = authenticationResponse.getDeviceIpAddress(); -``` - -Note that verificationCode should be displayed by the web service, so the person signing through the Smart-ID mobile app can verify if the verification code displayed on the phone matches with the one shown on the web page. -Leave a few seconds for the verification code to be displayed for users using the web service with their mobile device. -Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed). - -### Authenticating with document number - -If you already know the documentNumber you can use this for (re-)authentication. -Each document number is connected with specific mobile device of user. -If user has Smart-ID installed to multiple devices then this triggers notification to a specific device only. -This is why it is recommended to use authentication with document number if you want to target specific device only. - -```java -AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - -String verificationCode = authenticationHash.calculateVerificationCode(); - -// NB! Display verification code to the customer for a few seconds before starting next step: - -SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - // Smart-ID app will show 3 different verification codes to user and user must choose correct verification code - // before the user can enter PIN. If user selects wrong verification code then the operation will fail. - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .authenticate(); -``` - - - -## Validating authentication response - -It is mandatory to validate the authentication response. -Validation performs following checks: - -- signature is the valid signature over the same "hash", which was submitted by the RP. -- signature is the valid signature, verifiable with the public key inside the certificate of the user, given in the field "cert.value" -- returned certificate is valid (is not expired, signed by trusted CA and with correct level (i.e. not weaker than requested)) -- The identity of the authenticated person is in the 'subject' field of the included X.509 certificate. - -Validation returns information about the authenticated person. - -```java -// init Authentication response validator with trusted certificates loaded from within library -// as an alternative you can pass trusted certificates array as parameter to constructor -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); - -// throws SmartIdResponseValidationException if validation doesn't pass -AuthenticationIdentity authIdentity = authenticationResponseValidator.validate(authenticationResponse); - -String givenName = authIdentity.getGivenName(); // e.g. Mari-Liis" -String surname = authIdentity.getSurname(); // e.g. "Männik" -String identityCode = authIdentity.getIdentityCode(); // e.g. "47101010033" -String country = authIdentity.getCountry(); // e.g. "EE", "LV", "LT" -Optional dateOfBirth = authIdentity.getDateOfBirth(); // see next paragraph -``` - -### Extracting date-of-birth - -Since all Estonian and Lithuanian national identity numbers contain date-of-birth -this getDateOfBirth function always returns a correct value for them. - -For persons with Latvian national identity number the date-of-birth is parsed -from a separate field of the certificate but for some older Smart-id accounts -(issued between 2017-07-01 and 2021-05-20) the value might be missing. - -More info about the availability of the separate field in certificates: -https://github.com/SK-EID/smart-id-documentation/wiki/FAQ#where-can-i-find-users-date-of-birth - -```java -Optional dateOfBirth = authIdentity.getDateOfBirth(); -``` - -One can also only fetch the signing certificate of a person -and then construct authentication identity from that -and extract the date-of-birth from there. -Read below about how to obtain the signer's certificate. - -```java -AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(signersCertificate); -Optional dateOfBirthExtracted = identity.getDateOfBirth(); -``` - - -## Creating a signature - -### Obtaining signer's certificate - -To create a digital signature, most format require the signer's certificate beforehand. -To fetch the certificate you can use documentNumber. - -```java -SmartIdCertificate responseWithSigningCertificate = client - .getCertificate() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result - .withCertificateLevel("QUALIFIED") - .fetch(); - -X509Certificate signersCertificate = responseWithSigningCertificate.getCertificate(); -``` - -If needed you can use semantics identifier instead of document number to obtain signer's certificate. -This may trigger a notification to all the user's devices if user has more than one device with Smart-ID -(as each device has separate signing certificate). - -### Create the signature - -All Smart-ID devices support displaying text that is up to 60 characters long. -Some devices also support displaying text (on a separate screen) that is up to 200 characters long -as well as other interaction flows like user needs to choose the correct code from 3 different verification codes. - -You can send different interactions to user's device and it picks the first one that the app can handle. - -You need to use other utilities (like [DigiDoc4j](https://github.com/open-eid/digidoc4j) for example) to -create the AsicE/BDoc container with files in it and get the hash to be signed. - - -```java -SignableHash hashToSign = new SignableHash(); -hashToSign.setHashType(HashType.SHA256); -// calculate hash from the document you want to sign (i.e. use DigiDoc4j or other libraries) -// this class also has a method to set hash as byte array -hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - -// to display the verification code -String verificationCode = hashToSign.calculateVerificationCode(); - -// pause for a few seconds before starting following signing process - -SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here."), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - -byte[] signature = smartIdSignature.getValue(); - -smartIdSignature.getInteractionFlowUsed(); // which interaction was used -``` - -## Setting the order of preferred interactions for displaying text and asking PIN - -The app can support different interaction flows and a Relying Party can demand a particular flow with or without a fallback possibility. -Different interaction flows can support different amount of data to display information to user. - -Available interactions: -* `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. -* `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. -* `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. -* `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. - -RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. -The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interaction supported by the app is taken from `allowedInteractionsOrder` list and sent to client. -The interaction that was actually used is reported back to RP with interactionFlowUsed response parameter to the session request. -If the app cannot support any interaction requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. - -`displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. - -### Parameter allowedInteractionsOrder most common examples - -Following allowedInteractionsOrder combinations are most likely to be used. - -#### Short confirmation message with PIN - -If confirmation message fits to 60 characters then this is the most common choice. -Every Smart-ID app supports this interaction flow and there is no need to provide any fallbacks to this interaction. - -```java -SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("My confirmation message that is no more than 60 chars") - )) - .sign(); -``` - -#### Verification code choice - -This is more secure than previous example as the app forces user to look up the verification code displayed to him and -pick the same verification code from 3 different codes displayed in Smart-ID app and thus tries to assure that user is not interacting with some other service. - -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. - -If user's app doesn't support displaying verification code choice then system falls back to displaying text and PIN input. - -```java -try { - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Arrays.asList( - Interaction.verificationCodeChoice("My confirmation message that is no more than 60 chars"), - Interaction.displayTextAndPIN( "My confirmation message that is no more than 60 chars") - )) - .sign(); -} -catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException) { - System.out.println("User selected wrong verification code from 3-code choice"); -} -``` - -#### Long confirmation message with fallback to PIN - -Relying Party first choice is confirmationMessage that can be up to 200 characters long. -If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interaction. - - -```java -SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here."), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - -if (InteractionFlow.CONFIRMATION_MESSAGE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app was able to display full text to user"); -} -else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text to user"); -} -``` - -#### Long confirmation message together with verification code choice with fallback to verification code choice - -Relying Party first choice is confirmationMessage followed by verification code choice. -If this is not available then only verification code choice with shorter text is displayed. - -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. - - -```java -SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Long text (up to 200 characters) goes here."), - Interaction.verificationCodeChoice("Shorter text for less capable devices"), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - -if (InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app was able to display full text on separate screen and verification code choice."); -} -else if (InteractionFlow.VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text together with verification choice."); -} -else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text to user with PIN input."); -} -``` - - -### Interactions with longer text without fallback - -Relying Party can require interactions without fallback. -If End User's phone doesn't support required flow the library throws `RequiredInteractionNotSupportedByAppException`. - -```java -try { - client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here.") - )) - .sign(); -} -catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("User's Smart-ID app is not capable of displaying required interaction"); -} - -``` -## Handling exceptions - -Exceptions thrown by this library are hierarchical. -This way it is possible to reduce error handling code to only handle generic parent exceptions when suitable. - -* SmartIdException - all exceptions thrown by Smart-ID client are subclass of this - * UserActionException - Exceptions that are caused by user's actions (or lack of any action when needed) - * SessionTimeoutException - user didn't press anything in app when asked - * UserRefusedException - User pressed cancel. Usually handling this parent exception is enough but also has subclasses to indicate the exact screen where cancel was pressed. - * UserRefusedCertChoiceException - * UserRefusedConfirmationMessageException - * UserRefusedConfirmationMessageWithVerificationChoiceException - * UserRefusedDisplayTextAndPinException - * UserRefusedVerificationChoiceException - * UserSelectedWrongVerificationCodeException - the end user was displayed 3 codes in app and user selected wrong code - * UserAccountException - Exceptions that are caused by user account configuration. - * CertificateLevelMismatchException - * NoSuitableAccountOfRequestedTypeFoundException - * PersonShouldViewSmartIdPortalException - * DocumentUnusableException - * RequiredInteractionNotSupportedByAppException - * UserAccountNotFoundException - * Enduring - Exceptions that indicate problems with incorrect integration. - Usually these types of errors remain when user retries shortly. - * ServerMaintenanceException - Server is currently under maintenance - * SmartIdClientException - this exception is a sign of incorrect integration with Smart-ID service (i.e. missing parameters etc.) - * RelyingPartyAccountConfigurationException - indicates that RelyingParty configuration at Smart-ID side can be incorrect - * UnprocessableSmartIdResponseException - shouldn't happen under normal conditions - * SessionNotFoundException - When session was not found. Usually this is also caused by problems with implementation. - - -## Network connection configuration of the client - -Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: - -- Initiation request -- Session status request - -Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. - -```java -SmartIdClient client = new SmartIdClient(); -// ... -// sets the timeout for each session status poll -client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); -// sets the pause between each session status poll -client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); -``` - -As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. - -Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig(); -clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); -clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); - -client.setNetworkConnectionConfig(clientConfig); -``` -And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); -RequestConfig reqConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(30000) - .setConnectionRequestTimeout(5000) - .build(); -clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); - -client.setNetworkConnectionConfig(clientConfig); -``` - -Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. - -### Example of creating a client with configured ssl context on JBoss using JAXWS RS - - -```java -ResteasyClient resteasyClient = new ResteasyClientBuilder() - .sslContext(SmartIdClient.createSslContext(Arrays.asList( - "pem cert 1", "pem cert 2"))) - .build(); - -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); -client.setConfiguredClient(resteasyClient); -``` - -## Configuring a proxy - -If you need to access the internet through a proxy (that runs on 127.0.0.1:3128 in the examples) -you have two alternatives: - -### Configuring a proxy using JBoss Resteasy library - - -```java - org.jboss.resteasy.client.jaxrs.ResteasyClient resteasyClient = - new org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl() - .defaultProxy("127.0.0.1", 3128, "http") - .build(); - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setConfiguredClient(resteasyClient); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); -``` +## Setting up SmartIdClient for v3.1 -### Example of creating a client with configured proxy on JBoss - - ```java - org.glassfish.jersey.client.ClientConfig clientConfig = - new org.glassfish.jersey.client.ClientConfig(); - clientConfig.property(ClientProperties.PROXY_URI, "http://127.0.0.1:3128"); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setNetworkConnectionConfig(clientConfig); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); -``` - -# How to use API v3.0 +import ee.sk.smartid.SmartIdClient; -Support for Smart-ID API v3.0 has been added to the library. The code for v3.0 is located under the ee.sk.smartid.v3 package. -This version introduces new dynamic link and notification-based flows for authentication, certificate choice and signing. +InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore. -NB! v2 API classes are still available under the ee.sk.smartid.v2 package. -Some classes that were not specific to only v2 have not been moved. Aim was to provide easier way to migrate from v2 to v3. -For example v3 dynamic-link authentication can be still implemented so v2 signing stays the same. This way incremental migration is possible. +load(is, "changeit".toCharArray()); -To use the v3.0 API, import the relevant classes from the ee.sk.smartid.v3 package. -```java - import ee.sk.smartid.v3.SmartIdClient; - import ee.sk.smartid.v3.SmartIdConnector; -``` +var client = new SmartIdClient(); +client. -## Setting up SmartIdClient for v3.0 +setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client. -```java -import ee.sk.smartid.v3.SmartIdClient; +setRelyingPartyName("DEMO"); +client. -InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); +setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client. -var client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -client.setTrustStore(trustStore); +setTrustStore(trustStore); ``` ## Dynamic-link flows @@ -751,7 +197,7 @@ More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/ ##### Initiating an anonymous authentication session -Anonymous authentication is a new feature in Smart-ID API v3.0. It allows to authenticate users without knowing their identity. +Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to authenticate users without knowing their identity. RP can learn the user's identity only after the user has authenticated themselves. ```java @@ -862,7 +308,7 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ### Dynamic-link certificate choice session !!!Dynamic-link Certificate Choice session Cannot be used at the moment!!! -The Smart-ID API v3.0 introduces dynamic-link certificate choice session. This allows more secure way of initiating signing. +The Smart-ID API v3.1 introduces dynamic-link certificate choice session. This allows more secure way of initiating signing. Scanning QR-code or clicking on dynamic link will prove that the certificates of the device being used for signing is in the proximity where the signing was initiated. #### Request Parameters @@ -1209,9 +655,9 @@ String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "pn // Return Data URI to frontend and display the QR-code ``` -## Session status request handling for v3.0 +## Session status request handling for v3.1 -The Smart-ID v3.0 API includes new session status request path for retrieving session results. +The Smart-ID v3.1 API includes new session status request path for retrieving session results. Session status request is to be used for dynamic-link and notification-based flows. ### Session status response @@ -1230,7 +676,7 @@ The session status response includes various fields depending on whether the ses * `interactionFlowUsed`: The interaction flow used for the session. * `deviceIpAddress`: IP address of the mobile device, if requested. -### Examples of querying session status in v3.0 +### Examples of querying session status in v3.1 #### Example of using session status poller to query final sessions status @@ -1749,4 +1195,69 @@ Exception Categories * Validation and Parsing Exceptions These exceptions arise during validation or parsing operations within the library. * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. - * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. \ No newline at end of file + * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. + +## Network connection configuration of the client + +Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: + +- Initiation request +- Session status request + +Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. + +```java +SmartIdClient client = new SmartIdClient(); +// ... +// sets the timeout for each session status poll +client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); +// sets the pause between each session status poll +client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); +``` + +As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. + +Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig(); +clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); +clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); + +client.setNetworkConnectionConfig(clientConfig); +``` +And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); +RequestConfig reqConfig = RequestConfig.custom() + .setConnectTimeout(5000) + .setSocketTimeout(30000) + .setConnectionRequestTimeout(5000) + .build(); +clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); + +client.setNetworkConnectionConfig(clientConfig); +``` + +Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. + +### Example of creating a client with configured ssl context on JBoss using JAXWS RS + + +```java +ResteasyClient resteasyClient = new ResteasyClientBuilder() + .sslContext(SmartIdClient.createSslContext(Arrays.asList( + "pem cert 1", "pem cert 2"))) + .build(); + +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setConfiguredClient(resteasyClient); +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0b78f58e..4bfa6833 100644 --- a/pom.xml +++ b/pom.xml @@ -258,7 +258,7 @@ org.owasp dependency-check-maven - 10.0.4 + 12.1.1 true false diff --git a/private.key.enc b/private.key.enc deleted file mode 100644 index 9b87790501d527be63bb23a657e1865d45dbbe3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2560 zcmV+b3jg)ZRN*t`?Ay%F|AN{VLT?I@OVHt!SX*@6nLrksX0WQ-#sWpKXISz?(1y2X z!=(i;b4x%jkKx#g%oZut+ZkXpU9ioNG3Z^%o){ky<&8zKj$g+dkKQ(3I)AJYm+ zG;L{238aN;HlIzp{GF(??DS88+0lsDu7nfPsWML_o5Jpzp{$U=mCYw#-~H)L@@;1> zx>d-N4=9rqYqb@0$}4m~CN+H;1gV*}aU70*OTFa!Uv|?#NI!THY^5fsg2Tv>+WpTv z>`W@`w9}hA3I~$oMB>N4hoe!J34}JJlY8WPnG@qFFj!Uw4{Vdv50O2VKg(jb-}~UZ z%A{o)N)w5J&ChLn)HB3jv%tiRagZ?h<)ksN^AZY@FF=d}K^aH7g@S`*h4Xm<2t5XE z{4XV7XGDwFZdY71^N|{?@c?n9E?Hcx0706EiYnh477-WL=J{hvmhh2=|_y znvYxreecsd(FyU?W8AOcfJP=xAca|SiSp#7I&AQoZo^77-7MP__JAZF$5>fL*Rg#W zq#=90*IbL8{xLgLLc7Ea>Yo&1(G-Ta1s|`^jx^J* zvN#oig1$FxVGWZ$hqD4Wi98qRqcbXv;g`X`!pp#O0@~rLhyU;bBLs49v5&fB+L+!T zaZxKw@H7N{naj;{fGcxKXhWjUu6WV-x8t_Za+O)|&Uo z+Ne&|(au6ck{Pr+shq#Te-}im7gfS?w(Lda?U5_Tq|Z#^R-(`{>p=S4qbu);hB!^M zsd`*vT8iYNM&t;ze*3sQKbD`3m?vf-@WsO2tgm6mHNzFF5xb)^=ZpOFBKj=cg z?3uGi+JAJWQ)KXYiaAbHzjZNCf?RfS~U+qQ>8yJX1gJqub5HJ9;GsI#8z&Gl&OvndkUpguVil@wY&CfJVD*5VF> zoZ{I=goU}l$NI3wp~i{!Ly=oEKLIgrgQwfm5+BRc#A#Whtd@XaUd#Hkk$x<=ngz>X zBnrG0B!a60k53{ARf(y!cHsWq=2jlh=Fz4+RTa)J5hhATP^!_?p14x)bRS+~H0e8pW2Z2ZrLsm*}Ff<%9Q?2|7y3(ETB~I{&Tn;(Xpw=Ag{_ zKdBsW6xec0Ymmf8XuMeET}154fQ$7qKyPukAWILWG5qq@0yV9AH|tb)ka^N%+DO8T z#}e9sWXI>Qj(5;gVoDD(2HKba^v3xA*0P6^DM7>IXMy&Fj_)xkH3d}9GEw~U1slGV zaX%_?6`r^DaFCe7`rkCo)+rq@Sk@~y+S>+JwjmM$x6H&j35 zADDInSkuqZ@|pM+Wr1pG_RTdbw>}&8zS&xu;zlZ(&&u-7)rmshT*0Cfiy;-K{r&jO=PL`_}%@47l%g04L{X2M*DYUuXMeV@}A%upY)^ zJ*#7JGD5$awAibYHT8Bjjl=w29*5l89p2gImqsy4khZ$T*9W^}N+zr(Q1!hAc2u{| zJe~|@rLKR^Ir|lTRuRI!#~#dxzkmKD9^7 z7Cr6IwDpRa2O&G^yTPacG$T*(MZpA<^F(?~Hvyav2NyYoa}M2F#&K5+u!|7d-VJ6T z^O{@Mpp&lwLCE~%fIHfK_F~ipTdQuN>yKP%9m8WF3dGR_f{f=cDV(fuCGH#RQ0!M? znl3Cm$q_}ibo9|}v)E&(m0DB{PJyR*AWk!;`?>tj>D>SV>;7BmtjfkD5sqARK(;eL zRqFwWZunKnU5de@#sd#>mDugM5pw`Io)$#CB3-_CXdAQ~ z3TSUduoQDC?Jtuj@XG^F=;OpB;|cMOWl7giaX{Bq+X_H+9>L4R>2+Nf^AnKp`V#G; zF7RSFffi?gSx(_$rHXG?lR&6QQadd5!c#P@?QXqN$mUBJNU- zZGlY%sEm65f3uBVj#*2g!9L6Cd=b7%rfTfO^Ca*!G%) zQ99z89l9N%aU{RB=~FP(Zd6~|j~(U{hLbB%KjZh4*)eV*itasyvlZrS$!P@@45r+Z W4H?7RsadrLVmo6|uan{$u>+Un91(W_ diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 5bef29f8..00000000 --- a/publish.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -project="smart-id-java-client" - -version=$TRAVIS_TAG - -staging_url="https://oss.sonatype.org/service/local/staging/deploy/maven2/" -repositoryId="ossrh" - -artifact=$project-$version - -gpg --import ./private.key - -./mvnw versions:set -DnewVersion=$TRAVIS_TAG - -./mvnw package - -gpg -ab pom.xml - -cd target - -gpg -ab $artifact.jar -gpg -ab $artifact-sources.jar -gpg -ab $artifact-javadoc.jar - -jar -cvf bundle.jar ../pom.xml ../pom.xml.asc $artifact.jar $artifact.jar.asc $artifact-javadoc.jar $artifact-javadoc.jar.asc $artifact-sources.jar $artifact-sources.jar.asc - -curl -ujorlina2 -u $SONATYPEUN:$SONATYPEPW --request POST -F "file=@bundle.jar" "https://oss.sonatype.org/service/local/staging/bundle_upload" diff --git a/src/main/java/ee/sk/smartid/v3/AuthCode.java b/src/main/java/ee/sk/smartid/AuthCode.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/AuthCode.java rename to src/main/java/ee/sk/smartid/AuthCode.java index 049777c5..e1d18c3c 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthCode.java +++ b/src/main/java/ee/sk/smartid/AuthCode.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java rename to src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java index f15d1af8..f2606ec2 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java b/src/main/java/ee/sk/smartid/AuthenticationHash.java similarity index 96% rename from src/main/java/ee/sk/smartid/v2/AuthenticationHash.java rename to src/main/java/ee/sk/smartid/AuthenticationHash.java index 69090e2b..3323a5e9 100644 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/AuthenticationHash.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -28,9 +28,6 @@ import java.security.SecureRandom; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - /** * Class containing the hash and its hash type used for authentication */ diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java rename to src/main/java/ee/sk/smartid/AuthenticationResponse.java index a1108e4f..fa1cf8ac 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -30,7 +30,6 @@ import java.security.cert.X509Certificate; import java.util.Base64; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; /** diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java rename to src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index f38eb34e..49dae49d 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -31,14 +31,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; /** * Validates and maps the received session status to authentication response diff --git a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java similarity index 79% rename from src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java rename to src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index aa3b13a1..396d7dd7 100644 --- a/src/main/java/ee/sk/smartid/v3/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -44,11 +44,15 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.Objects; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.x500.X500Principal; import org.slf4j.Logger; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationIdentityMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; @@ -191,20 +195,73 @@ private void validateCertificateLevel(AuthenticationResponse authenticationRespo } } + private record CertDnDetails(String country, String organization, String commonName) { + + private static CertDnDetails from(X500Principal principal) { + String country = null; + String organization = null; + String commonName = null; + + LdapName ldapName; + try { + ldapName = new LdapName(principal.getName()); + } catch (InvalidNameException e) { + String errorMessage = "Error getting certificate distinguished name"; + logger.error(errorMessage, e); + throw new SmartIdClientException(errorMessage, e); + } + + for (Rdn rdn : ldapName.getRdns()) { + if ("C".equalsIgnoreCase(rdn.getType())) { + country = rdn.getValue().toString(); + } else if ("O".equalsIgnoreCase(rdn.getType())) { + organization = rdn.getValue().toString(); + } else if ("CN".equalsIgnoreCase(rdn.getType())) { + commonName = rdn.getValue().toString(); + } + } + return new CertDnDetails(country, organization, commonName); + } + + private static boolean equal(CertDnDetails first, CertDnDetails second) { + return Objects.equals(first.country, second.country) && + Objects.equals(first.organization, second.organization) && + Objects.equals(first.commonName, second.commonName); + } + } + private void validateCertificateIsTrusted(X509Certificate responseCertificate) { + CertDnDetails issuerDn = CertDnDetails.from(responseCertificate.getIssuerX500Principal()); + for (X509Certificate trustedCACertificate : trustedCACertificates) { + logger.debug("Verifying signer's certificate '{}' against CA certificate '{}'", + responseCertificate.getSubjectX500Principal(), + trustedCACertificate.getSubjectX500Principal()); + + CertDnDetails caCertDn = CertDnDetails.from(trustedCACertificate.getSubjectX500Principal()); + + if (!CertDnDetails.equal(issuerDn, caCertDn)) { + logger.debug("Skipped trusted CA certificate '{}', no match with signer's certificate issuer '{}'", + trustedCACertificate.getSubjectX500Principal(), + responseCertificate.getIssuerX500Principal()); + continue; + } + try { responseCertificate.verify(trustedCACertificate.getPublicKey()); - logger.info("Certificate verification passed for '{}' against CA responseCertificate '{}' ", + logger.info("Certificate verification passed for '{}' against CA certificate '{}'", responseCertificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal()); return; } catch (GeneralSecurityException ex) { - logger.debug("Error verifying signer's responseCertificate: {} against CA responseCertificate: {}", + logger.debug("Error verifying signer's certificate: {} against CA certificate: {}", responseCertificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal(), ex); } } + + logger.error("No suitable trusted CA certificate found: '{}'. Ensure that this CA certificate is present in the trusted CA certificate list", + responseCertificate.getIssuerX500Principal()); throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); } diff --git a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java rename to src/main/java/ee/sk/smartid/CertificateChoiceResponse.java index e35a1dff..aba8e37c 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java rename to src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java index 7f70b236..72c4a6a6 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateChoiceResponseMapper.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -30,14 +30,13 @@ import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; -import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionStatus; /** * Validates and maps the received session status to certificate choice response diff --git a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/CertificateLevel.java rename to src/main/java/ee/sk/smartid/CertificateLevel.java index a9ad98b3..d4a8684c 100644 --- a/src/main/java/ee/sk/smartid/v3/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java b/src/main/java/ee/sk/smartid/DynamicContentBuilder.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java rename to src/main/java/ee/sk/smartid/DynamicContentBuilder.java index dbba481e..f9e12ed3 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicContentBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicContentBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java index 697cf946..c3f502bf 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -36,12 +36,12 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; /** * Class for building a dynamic link authentication session request diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 62ab5d1e..2900626e 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -35,10 +35,10 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; public class DynamicLinkCertificateChoiceSessionRequestBuilder { diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java index be83270e..fb8f9fca 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -36,13 +36,13 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; public class DynamicLinkSignatureSessionRequestBuilder { diff --git a/src/main/java/ee/sk/smartid/v3/DynamicLinkType.java b/src/main/java/ee/sk/smartid/DynamicLinkType.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/DynamicLinkType.java rename to src/main/java/ee/sk/smartid/DynamicLinkType.java index fc195430..906f6d72 100644 --- a/src/main/java/ee/sk/smartid/v3/DynamicLinkType.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkType.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java rename to src/main/java/ee/sk/smartid/ErrorResultHandler.java index 7c50c837..9dee57a1 100644 --- a/src/main/java/ee/sk/smartid/v3/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index a4ff7725..612fdac5 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -37,14 +37,14 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.VerificationCode; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.VerificationCode; /** * Class for building a notification authentication session request diff --git a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 6ebeb1b5..09b097be 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -33,10 +33,10 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.RequestProperties; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; import java.util.Set; diff --git a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index ad69346c..ff23bdc0 100644 --- a/src/main/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -36,14 +36,14 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.Interaction; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.rest.dao.VerificationCode; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.VerificationCode; public class NotificationSignatureSessionRequestBuilder { diff --git a/src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java b/src/main/java/ee/sk/smartid/QrCodeGenerator.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java rename to src/main/java/ee/sk/smartid/QrCodeGenerator.java index 0c310561..4daed4e7 100644 --- a/src/main/java/ee/sk/smartid/v3/QrCodeGenerator.java +++ b/src/main/java/ee/sk/smartid/QrCodeGenerator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/RandomChallenge.java b/src/main/java/ee/sk/smartid/RandomChallenge.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/RandomChallenge.java rename to src/main/java/ee/sk/smartid/RandomChallenge.java index 128816fb..758a5004 100644 --- a/src/main/java/ee/sk/smartid/v3/RandomChallenge.java +++ b/src/main/java/ee/sk/smartid/RandomChallenge.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/SessionType.java b/src/main/java/ee/sk/smartid/SessionType.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/SessionType.java rename to src/main/java/ee/sk/smartid/SessionType.java index 85fce0c1..0acf1137 100644 --- a/src/main/java/ee/sk/smartid/v3/SessionType.java +++ b/src/main/java/ee/sk/smartid/SessionType.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/SignableData.java rename to src/main/java/ee/sk/smartid/SignableData.java index aa860507..40c4c2f1 100644 --- a/src/main/java/ee/sk/smartid/v3/SignableData.java +++ b/src/main/java/ee/sk/smartid/SignableData.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -29,9 +29,6 @@ import java.io.Serializable; import java.util.Base64; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - /** * This class can be used to contain the data * to be signed when it is not yet in hashed format diff --git a/src/main/java/ee/sk/smartid/v3/SignableHash.java b/src/main/java/ee/sk/smartid/SignableHash.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/SignableHash.java rename to src/main/java/ee/sk/smartid/SignableHash.java index 5da00753..583e4094 100644 --- a/src/main/java/ee/sk/smartid/v3/SignableHash.java +++ b/src/main/java/ee/sk/smartid/SignableHash.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -29,8 +29,6 @@ import java.io.Serializable; import java.util.Base64; -import ee.sk.smartid.HashType; - /** * This class can be used to contain the hash * to be signed diff --git a/src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java rename to src/main/java/ee/sk/smartid/SignatureAlgorithm.java index 2446c7f4..5c2ff4dc 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/SignatureProtocol.java rename to src/main/java/ee/sk/smartid/SignatureProtocol.java index 69a341cc..42dd88c5 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java similarity index 99% rename from src/main/java/ee/sk/smartid/v3/SignatureResponse.java rename to src/main/java/ee/sk/smartid/SignatureResponse.java index 00c39d55..4eff420e 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/SignatureResponseMapper.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java rename to src/main/java/ee/sk/smartid/SignatureResponseMapper.java index 9d28f65f..57391d62 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseMapper.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -33,7 +33,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.CertificateParser; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; @@ -42,10 +41,10 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; public class SignatureResponseMapper { diff --git a/src/main/java/ee/sk/smartid/v3/SignatureUtil.java b/src/main/java/ee/sk/smartid/SignatureUtil.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/SignatureUtil.java rename to src/main/java/ee/sk/smartid/SignatureUtil.java index c4bc3350..3e0fe555 100644 --- a/src/main/java/ee/sk/smartid/v3/SignatureUtil.java +++ b/src/main/java/ee/sk/smartid/SignatureUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -26,7 +26,6 @@ * #L% */ -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.permanent.SmartIdClientException; public class SignatureUtil { diff --git a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/SmartIdClient.java rename to src/main/java/ee/sk/smartid/SmartIdClient.java index 3dce30ae..e5989d51 100644 --- a/src/main/java/ee/sk/smartid/v3/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -44,9 +44,9 @@ import javax.net.ssl.TrustManagerFactory; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v3.rest.SessionStatusPoller; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.SmartIdRestConnector; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.Configuration; diff --git a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java similarity index 96% rename from src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java rename to src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index fcaba2b6..3d3b8eb7 100644 --- a/src/main/java/ee/sk/smartid/v2/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -28,9 +28,6 @@ import java.nio.ByteBuffer; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - public class VerificationCodeCalculator { /** diff --git a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java rename to src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java index 93a294f2..1079e602 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest; +package ee.sk.smartid.rest; /*- * #%L @@ -32,7 +32,7 @@ import org.slf4j.LoggerFactory; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionStatus; /** * Provides methods for querying sessions status and polling session status diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java similarity index 93% rename from src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java rename to src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 816f05d2..bb347330 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest; +package ee.sk.smartid.rest; /*- * #%L @@ -33,14 +33,14 @@ import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; public interface SmartIdConnector extends Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java similarity index 96% rename from src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java rename to src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 568edd5f..fd697917 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest; +package ee.sk.smartid.rest; /*- * #%L @@ -44,17 +44,16 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.LoggingFilter; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SessionStatusRequest; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionStatusRequest; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.ForbiddenException; diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java index 82e90787..81fdc7d8 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/AcspV1SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java rename to src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java index be4f5a51..03101122 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L @@ -31,7 +31,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.v3.SignatureProtocol; +import ee.sk.smartid.SignatureProtocol; public class AuthenticationSessionRequest implements Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java rename to src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java index 2a1181fb..e8861e5e 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/CertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java similarity index 91% rename from src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java rename to src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java index e90426a8..b8201a1a 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteraction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L @@ -26,8 +26,8 @@ * #L% */ -import static ee.sk.smartid.v3.rest.dao.DynamicLinkInteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.v3.rest.dao.DynamicLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; +import static ee.sk.smartid.rest.dao.DynamicLinkInteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.rest.dao.DynamicLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; public class DynamicLinkInteraction extends Interaction { diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java rename to src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java index ee93b397..78f27843 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkInteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java index 394cbc29..937d949e 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/DynamicLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java rename to src/main/java/ee/sk/smartid/rest/dao/Interaction.java index be725814..1af72165 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java rename to src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java index d4e9f5c0..4e5dff14 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java index 2d16b049..ca9cbeb7 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationAuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java similarity index 97% rename from src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java index 60c1a678..e4b60d87 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java similarity index 90% rename from src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java index 96ea5dd5..4268baf9 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteraction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L @@ -26,8 +26,8 @@ * #L% */ -import static ee.sk.smartid.v3.rest.dao.NotificationInteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; -import static ee.sk.smartid.v3.rest.dao.NotificationInteractionFlow.VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.rest.dao.NotificationInteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; +import static ee.sk.smartid.rest.dao.NotificationInteractionFlow.VERIFICATION_CODE_CHOICE; public class NotificationInteraction extends Interaction { diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java index 62d34d74..2b3ccba6 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationInteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java index 93db3c88..420c827b 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/NotificationSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index 0ffbf2e5..ab900a21 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java rename to src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index 794d0102..0e2d9d9b 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java rename to src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java index 7dcdea7d..70cc0f27 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java rename to src/main/java/ee/sk/smartid/rest/dao/SessionResult.java index 7c30bf63..0c107eea 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java rename to src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java index 3cca24da..45a5aade 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java rename to src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 6bb17728..3349bbc6 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java rename to src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java index e394c135..b3286e61 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java rename to src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java index ca28ed2e..6ba4f353 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L @@ -31,7 +31,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.v3.SignatureProtocol; +import ee.sk.smartid.SignatureProtocol; public class SignatureSessionRequest implements Serializable { diff --git a/src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java similarity index 98% rename from src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java rename to src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index 23112aed..e640eae6 100644 --- a/src/main/java/ee/sk/smartid/v3/rest/dao/VerificationCode.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest.dao; +package ee.sk.smartid.rest.dao; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java deleted file mode 100644 index eb9dca76..00000000 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationRequestBuilder.java +++ /dev/null @@ -1,395 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionCertificate; -import ee.sk.smartid.v2.rest.dao.SessionResult; -import ee.sk.smartid.v2.rest.dao.SessionSignature; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.SmartIdConnector; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import static ee.sk.smartid.util.StringUtil.isNotEmpty; - -/** - * Class for building authentication request and getting the response - *

      - * Mandatory request parameters: - *

        - *
      • Host url - can be set on the {@link SmartIdClient} level
      • - *
      • Relying party uuid - can either be set on the client or builder level
      • - *
      • Relying party name - can either be set on the client or builder level
      • - *
      • Either Document number or semantics identifier or private company identifier
      • - *
      • Authentication hash
      • - *
      - * Optional request parameters: - *
        - *
      • Certificate level
      • - *
      • Display text
      • - *
      • Nonce
      • - *
      - */ -public class AuthenticationRequestBuilder extends SmartIdRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticationRequestBuilder.class); - -/** - * Constructs a new {@code AuthenticationRequestBuilder} - * - * @param connector for requesting authentication initiation - * @param sessionStatusPoller for polling the authentication response - */ - public AuthenticationRequestBuilder(SmartIdConnector connector, SessionStatusPoller sessionStatusPoller) { - super(connector, sessionStatusPoller); - logger.debug("Instantiating authentication request builder"); - } - - /** - * Sets the request's UUID of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set the UUID every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - * @return this builder - */ - public AuthenticationRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the request's name of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set name every time when building a new request. - * - * @param relyingPartyName name of the relying party - * @return this builder - */ - public AuthenticationRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the request's document number - *

      - * Document number is unique for the user's certificate/device - * that is used for the authentication. - * - * @param documentNumber document number of the certificate/device to be authenticated - * @return this builder - */ - public AuthenticationRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, a hyphen and the identifier. - * - * @param semanticsIdentifier semantics identifier for a person - * @return this builder - */ - public AuthenticationRequestBuilder withSemanticsIdentifierAsString(String semanticsIdentifier) { - this.semanticsIdentifier = new SemanticsIdentifier(semanticsIdentifier); - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, and the identifier. - * - * @param semanticsIdentifier semantics identifier for a person - * @return this builder - */ - public AuthenticationRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the request's authentication hash - *

      - * It is the hash that is signed by a person's device - * which is essential for the authentication verification. - * For security reasons the hash should be generated - * randomly for every new request. It is recommended to use: - * {@link AuthenticationHash#generateRandomHash()} - * - * @param authenticationHash hash used to sign for authentication - * @return this builder - */ - public AuthenticationRequestBuilder withAuthenticationHash(AuthenticationHash authenticationHash) { - this.hashToSign = authenticationHash; - return this; - } - - /** - * Sets the request's certificate level - *

      - * Defines the minimum required level of the certificate. - * Optional. When not set, it defaults to what is configured - * on the server side i.e. "QUALIFIED". - * - * @param certificateLevel the level of the certificate - * @return this builder - */ - public AuthenticationRequestBuilder withCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the request's nonce - *

      - * By default, the authentication's initiation request - * has idempotent behaviour meaning when the request - * is repeated inside a given time frame with exactly - * the same parameters, session ID of an existing session - * can be returned as a result. When requester wants, it can - * override the idempotent behaviour inside of this time frame - * using an optional "nonce" parameter present for all POST requests. - *

      - * Normally, this parameter can be omitted. - * - * @param nonce nonce of the request - * @return this builder - */ - public AuthenticationRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Specifies capabilities of the user - *

      - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(String...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of [QUALIFIED, ADVANCED] - * @return this builder - */ - public AuthenticationRequestBuilder withCapabilities(Capability... capabilities) { - this.capabilities = Arrays.stream(capabilities).map(Objects::toString).collect(Collectors.toSet()); - return this; - } - - /** - * Specifies capabilities of the user - *

      - * - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(Capability...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of ["QUALIFIED", "ADVANCED"] - * @return this builder - */ - public AuthenticationRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = new HashSet<>(Arrays.asList(capabilities)); - return this; - } - - /** - * @param allowedInteractionsOrder Preferred order of what dialog to present to user. What actually gets displayed depends on user's device and its software version. - * First option from this list that the device is capable of handling is displayed to the user. - * @return this builder - */ - public AuthenticationRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; - return this; - } - - /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. - * @see Mobile Device IP sharing - * - * @return this builder - */ - public AuthenticationRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Send the authentication request and get the response - *

      - * This method uses automatic session status polling internally - * and therefore blocks the current thread until authentication is concluded/interrupted etc. - * - * @throws UserAccountNotFoundException when the user account was not found - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason. - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return the authentication response - */ - public SmartIdAuthenticationResponse authenticate() throws UserAccountNotFoundException, UserRefusedException, - UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException, ServerMaintenanceException { - String sessionId = initiateAuthentication(); - SessionStatus sessionStatus = getSessionStatusPoller().fetchFinalSessionStatus(sessionId); - return createSmartIdAuthenticationResponse(sessionStatus); - } - - /** - * Send the authentication request and get the session ID - * - * @throws UserAccountNotFoundException when the user account was not found - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return session ID - later to be used for manual session status polling - */ - public String initiateAuthentication() throws UserAccountNotFoundException, ServerMaintenanceException { - validateParameters(); - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - AuthenticationSessionResponse response = getAuthenticationResponse(request); - return response.getSessionID(); - } - - /** - * Create {@link SmartIdAuthenticationResponse} from {@link SessionStatus} - * - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * - * @param sessionStatus session status response - * @return the authentication response - */ - public SmartIdAuthenticationResponse createSmartIdAuthenticationResponse(SessionStatus sessionStatus) throws UserRefusedException, UserSelectedWrongVerificationCodeException, - SessionTimeoutException, DocumentUnusableException { - validateAuthenticationResponse(sessionStatus); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate certificate = sessionStatus.getCert(); - - SmartIdAuthenticationResponse authenticationResponse = new SmartIdAuthenticationResponse(); - authenticationResponse.setEndResult(sessionResult.getEndResult()); - authenticationResponse.setSignedHashInBase64(getHashInBase64()); - authenticationResponse.setHashType(getHashType()); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - authenticationResponse.setAlgorithmName(sessionSignature.getAlgorithm()); - authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - authenticationResponse.setRequestedCertificateLevel(getCertificateLevel()); - authenticationResponse.setCertificateLevel(certificate.getCertificateLevel()); - authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - authenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return authenticationResponse; - } - - protected void validateParameters() { - super.validateParameters(); - super.validateAuthSignParameters(); - } - - private void validateAuthenticationResponse(SessionStatus sessionStatus) { - validateSessionResult(sessionStatus.getResult()); - if (sessionStatus.getSignature() == null) { - logger.error("Signature was not present in the response"); - throw new UnprocessableSmartIdResponseException("Signature was not present in the response"); - } - if (sessionStatus.getCert() == null) { - logger.error("Certificate was not present in the response"); - throw new UnprocessableSmartIdResponseException("Certificate was not present in the response"); - } - } - - private AuthenticationSessionResponse getAuthenticationResponse(AuthenticationSessionRequest request) { - SemanticsIdentifier semanticsIdentifier = getSemanticsIdentifier(); - if (isNotEmpty(getDocumentNumber())) { - return getConnector().authenticate(getDocumentNumber(), request); - } - else { - return getConnector().authenticate(semanticsIdentifier, request); - } - } - - private AuthenticationSessionRequest createAuthenticationSessionRequest() { - AuthenticationSessionRequest request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(getRelyingPartyUUID()); - request.setRelyingPartyName(getRelyingPartyName()); - request.setCertificateLevel(getCertificateLevel()); - request.setHashType(getHashTypeString()); - request.setHash(getHashInBase64()); - request.setNonce(getNonce()); - request.setCapabilities(getCapabilities()); - request.setAllowedInteractionsOrder(getAllowedInteractionsOrder()); - - RequestProperties requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - if (requestProperties.hasProperties()) { - request.setRequestProperties(requestProperties); - } - - return request; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java deleted file mode 100644 index c1ca6a9e..00000000 --- a/src/main/java/ee/sk/smartid/v2/AuthenticationResponseValidator.java +++ /dev/null @@ -1,281 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static java.util.Arrays.asList; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Date; -import java.util.Enumeration; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationIdentityMapper; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.util.StringUtil; - -/** - * Class used to validate the authentication - */ -public class AuthenticationResponseValidator { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseValidator.class); - - private final List trustedCACertificates = new ArrayList<>(); - - /** - * Constructs a new {@code AuthenticationResponseValidator}. - *

      - * The constructed instance is initialized with default trusted - * CA certificates. - * - * @throws SmartIdClientException when there was an error initializing trusted CA certificates - */ - public AuthenticationResponseValidator() { - initializeTrustedCACertificatesFromKeyStore(); - } - - /** - * Constructs a new {@code AuthenticationResponseValidator}. - *

      - * The constructed instance is initialized passed in certificates. - * - * @param trustedCertificates List of certificates to trust - * @throws SmartIdClientException when there was an error initializing trusted CA certificates - */ - public AuthenticationResponseValidator(X509Certificate[] trustedCertificates) { - trustedCACertificates.addAll(asList(trustedCertificates)); - } - - /** - * Validates the authentication response and returns the result. - * Performs the following validations: - * "result.endResult" has the value "OK" - * "signature.value" is the valid signature over the same "hash", which was submitted by the RP. - * "signature.value" is the valid signature, verifiable with the public key inside the certificate of the user, given in the field "cert.value" - * The person's certificate given in the "cert.value" is valid (not expired, signed by trusted CA and with correct (i.e. the same as in response structure, greater than or equal to that in the original request) level). - * - * @param authenticationResponse authentication response to be validated - * @return authentication result - */ - public AuthenticationIdentity validate(SmartIdAuthenticationResponse authenticationResponse) { - validateAuthenticationResponse(authenticationResponse); - AuthenticationIdentity identity = constructAuthenticationIdentity(authenticationResponse.getCertificate()); - if (!verifyResponseEndResult(authenticationResponse)) { - throw new UnprocessableSmartIdResponseException("Smart-ID API returned end result code '" + authenticationResponse.getEndResult() + "'"); - } - if (!verifySignature(authenticationResponse)) { - throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); - } - if (!verifyCertificateExpiry(authenticationResponse.getCertificate())) { - throw new UnprocessableSmartIdResponseException("Signer's certificate has expired"); - } - if (!isCertificateTrusted(authenticationResponse.getCertificate())) { - throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); - } - if (!verifyCertificateLevel(authenticationResponse)) { - throw new CertificateLevelMismatchException(); - } - return identity; - } - - /** - * Gets the list of trusted CA certificates - *

      - * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @return list of trusted CA certificates - */ - public List getTrustedCACertificates() { - return trustedCACertificates; - } - - /** - * Adds a certificate to the list of trusted CA certificates - *

      - * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @param certificate trusted CA certificate - */ - public void addTrustedCACertificate(X509Certificate certificate) { - trustedCACertificates.add(certificate); - } - - /** - * Constructs a certificate from the byte array and - * adds it into the list of trusted CA certificates - *

      - * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @param certificateBytes trusted CA certificate - * @throws CertificateException when there was an error constructing the certificate from bytes - */ - public void addTrustedCACertificate(byte[] certificateBytes) throws CertificateException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - X509Certificate caCertificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); - addTrustedCACertificate(caCertificate); - } - - /** - * Constructs a certificate from the file - * and adds it into the list of trusted CA certificates - *

      - * Authenticating person's certificate has to be issued by - * one of the trusted CA certificates. Otherwise, the person's - * authentication is deemed untrusted and therefore not valid. - * - * @param certificateFile trusted CA certificate - * @throws IOException when there is an error reading the file - * @throws CertificateException when there is an error constructing the certificate from the bytes of the file - */ - public void addTrustedCACertificate(File certificateFile) throws IOException, CertificateException { - addTrustedCACertificate(Files.readAllBytes(certificateFile.toPath())); - } - - /** - * Clears the list of trusted CA certificates - *

      - * PS! When clearing the trusted CA certificates - * make sure it is not left empty. In that case - * there is impossible to verify the trust of the - * authenticating person. - */ - public void clearTrustedCACertificates() { - trustedCACertificates.clear(); - } - - public static AuthenticationIdentity constructAuthenticationIdentity(X509Certificate certificate) { - return AuthenticationIdentityMapper.from(certificate); - } - - private void initializeTrustedCACertificatesFromKeyStore() { - try (InputStream is = AuthenticationResponseValidator.class.getResourceAsStream("/trusted_certificates.jks")) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, "changeit".toCharArray()); - Enumeration aliases = keystore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - addTrustedCACertificate(certificate); - } - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing trusted CA certificates", e); - throw new SmartIdClientException("Error initializing trusted CA certificates", e); - } - } - - private void validateAuthenticationResponse(SmartIdAuthenticationResponse authenticationResponse) { - if (authenticationResponse.getCertificate() == null) { - logger.error("Certificate is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Certificate is not present in the authentication response"); - } - if (StringUtil.isEmpty(authenticationResponse.getSignatureValueInBase64())) { - logger.error("Signature is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Signature is not present in the authentication response"); - } - if (authenticationResponse.getHashType() == null) { - logger.error("Hash type is not present in the authentication response"); - throw new UnprocessableSmartIdResponseException("Hash type is not present in the authentication response"); - } - } - - private boolean verifyResponseEndResult(SmartIdAuthenticationResponse authenticationResponse) { - return "OK".equalsIgnoreCase(authenticationResponse.getEndResult()); - } - - private boolean verifySignature(SmartIdAuthenticationResponse authenticationResponse) { - try { - PublicKey signersPublicKey = authenticationResponse.getCertificate().getPublicKey(); - Signature signature = Signature.getInstance("NONEwith" + signersPublicKey.getAlgorithm()); - signature.initVerify(signersPublicKey); - byte[] signedHash = Base64.getDecoder().decode(authenticationResponse.getSignedHashInBase64()); - byte[] signedDigestWithPadding = addPadding(authenticationResponse.getHashType().getDigestInfoPrefix(), signedHash); - signature.update(signedDigestWithPadding); - return signature.verify(authenticationResponse.getSignatureValue()); - } catch (GeneralSecurityException e) { - logger.error("Signature verification failed"); - throw new UnprocessableSmartIdResponseException("Signature verification failed", e); - } - } - - private boolean verifyCertificateExpiry(X509Certificate certificate) { - return !certificate.getNotAfter().before(new Date()); - } - - private boolean isCertificateTrusted(X509Certificate certificate) { - for (X509Certificate trustedCACertificate : trustedCACertificates) { - try { - certificate.verify(trustedCACertificate.getPublicKey()); - logger.info("Certificate verification passed for '{}' against CA certificate '{}' ", certificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal()); - - return true; - } catch (GeneralSecurityException e) { - logger.debug("Error verifying signer's certificate: {} against CA certificate: {}", certificate.getSubjectX500Principal(), trustedCACertificate.getSubjectX500Principal(), e); - } - } - return false; - } - - private boolean verifyCertificateLevel(SmartIdAuthenticationResponse authenticationResponse) { - CertificateLevel certLevel = new CertificateLevel(authenticationResponse.getCertificateLevel()); - String requestedCertificateLevel = authenticationResponse.getRequestedCertificateLevel(); - return StringUtil.isEmpty(requestedCertificateLevel) || certLevel.isEqualOrAbove(requestedCertificateLevel); - } - - private static byte[] addPadding(byte[] digestInfoPrefix, byte[] digest) { - final byte[] digestWithPrefix = new byte[digestInfoPrefix.length + digest.length]; - System.arraycopy(digestInfoPrefix, 0, digestWithPrefix, 0, digestInfoPrefix.length); - System.arraycopy(digest, 0, digestWithPrefix, digestInfoPrefix.length, digest.length); - return digestWithPrefix; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/CertificateLevel.java b/src/main/java/ee/sk/smartid/v2/CertificateLevel.java deleted file mode 100644 index 6478c2ad..00000000 --- a/src/main/java/ee/sk/smartid/v2/CertificateLevel.java +++ /dev/null @@ -1,60 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import java.util.HashMap; -import java.util.Map; - -public class CertificateLevel { - - private final String certificateLevel; - - private static final Map certificateLevels = new HashMap<>(); - - static { - certificateLevels.put("ADVANCED", 1); - certificateLevels.put("QUALIFIED", 2); - } - - public CertificateLevel(String certificateLevel) { - if (certificateLevel == null) { - throw new IllegalArgumentException("certificateLevel cannot be null"); - } - this.certificateLevel = certificateLevel; - } - - public boolean isEqualOrAbove(String certificateLevel) { - if (this.certificateLevel.equalsIgnoreCase(certificateLevel)) { - return true; - } - else if (certificateLevels.get(certificateLevel) != null && certificateLevels.get(this.certificateLevel) != null) { - return certificateLevels.get(certificateLevel) <= certificateLevels.get(this.certificateLevel); - } - return false; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java deleted file mode 100644 index 7505a4bb..00000000 --- a/src/main/java/ee/sk/smartid/v2/CertificateRequestBuilder.java +++ /dev/null @@ -1,356 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionCertificate; -import ee.sk.smartid.v2.rest.dao.SessionResult; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.SmartIdConnector; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.stream.Collectors; - -import static ee.sk.smartid.util.StringUtil.isEmpty; -import static ee.sk.smartid.util.StringUtil.isNotEmpty; - -/** - * Class for building certificate choice request and getting the response - *

      - * Mandatory request parameters: - *

        - *
      • Host url - can be set on the {@link SmartIdClient} level
      • - *
      • Relying party uuid - can either be set on the client or builder level
      • - *
      • Relying party name - can either be set on the client or builder level
      • - *
      • Either Document number or national identity
      • - *
      - * Optional request parameters: - *
        - *
      • Certificate level
      • - *
      • Nonce
      • - *
      - */ -public class CertificateRequestBuilder extends SmartIdRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(CertificateRequestBuilder.class); - - /** - * Constructs a new {@code CertificateRequestBuilder} - * - * @param connector for requesting certificate choice initiation - * @param sessionStatusPoller for polling the certificate choice response - */ - public CertificateRequestBuilder(SmartIdConnector connector, SessionStatusPoller sessionStatusPoller) { - super(connector, sessionStatusPoller); - logger.debug("Instantiating certificate request builder"); - } - - /** - * Sets the request's UUID of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set the UUID every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - * @return this builder - */ - public CertificateRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - super.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the request's name of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set name every time when building a new request. - * - * @param relyingPartyName name of the relying party - * @return this builder - */ - public CertificateRequestBuilder withRelyingPartyName(String relyingPartyName) { - super.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the request's document number - *

      - * Document number is unique for the user's certificate/device - * that is used for choosing the certificate. - * - * @param documentNumber document number of the certificate/device used to choose the certificate - * @return this builder - */ - public CertificateRequestBuilder withDocumentNumber(String documentNumber) { - super.documentNumber = documentNumber; - return this; - } - - /** - * Sets the request's certificate level - *

      - * Defines the minimum required level of the certificate. - * Optional. When not set, it defaults to what is configured - * on the server side i.e. "QUALIFIED". - * - * @param certificateLevel the level of the certificate - * @return this builder - */ - public CertificateRequestBuilder withCertificateLevel(String certificateLevel) { - super.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the request's nonce - *

      - * By default, the certificate choice's initiation request - * has idempotent behaviour meaning when the request - * is repeated inside a given time frame with exactly - * the same parameters, session ID of an existing session - * can be returned as a result. When requester wants, it can - * override the idempotent behaviour inside of this time frame - * using an optional "nonce" parameter present for all POST requests. - *

      - * Normally, this parameter can be omitted. - * - * @param nonce nonce of the request - * @return this builder - */ - public CertificateRequestBuilder withNonce(String nonce) { - super.nonce = nonce; - return this; - } - - /** - * Specifies capabilities of the user - *

      - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(String...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of [QUALIFIED, ADVANCED] - * @return this builder - */ - public CertificateRequestBuilder withCapabilities(Capability... capabilities) { - this.capabilities = Arrays.stream(capabilities).map(Objects::toString).collect(Collectors.toSet()); - return this; - } - - /** - * Specifies capabilities of the user - *

      - * - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(Capability...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of ["QUALIFIED", "ADVANCED"] - * @return this builder - */ - public CertificateRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = new HashSet<>(Arrays.asList(capabilities)); - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, a hyphen and the identifier. - * - * @param semanticsIdentifierAsString semantics identifier for a person - * @return this builder - */ - public CertificateRequestBuilder withSemanticsIdentifierAsString(String semanticsIdentifierAsString) { - super.semanticsIdentifier = new SemanticsIdentifier(semanticsIdentifierAsString); - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, and the identifier. - * - * @param semanticsIdentifier semantics identifier for a person - * @return this builder - */ - public CertificateRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - super.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. - * @see Mobile Device IP sharing - * - * @return this builder - */ - public CertificateRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Send the certificate choice request and get the response - *x - * @throws UserAccountNotFoundException when the certificate was not found - * @throws UserRefusedException when the user has refused the session. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason. - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return the certificate choice response - */ - public SmartIdCertificate fetch() throws UserAccountNotFoundException, UserRefusedException, - SessionTimeoutException, DocumentUnusableException, SmartIdClientException, ServerMaintenanceException { - logger.debug("Starting to fetch certificate"); - validateParameters(); - String sessionId = initiateCertificateChoice(); - SessionStatus sessionStatus = getSessionStatusPoller().fetchFinalSessionStatus(sessionId); - return createSmartIdCertificate(sessionStatus); - } - - /** - * Send the certificate choice request and get the session ID - * - * @throws UserAccountNotFoundException when the user account was not found - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return session ID - later to be used for manual session status polling - */ - public String initiateCertificateChoice() throws UserAccountNotFoundException, - SmartIdClientException, ServerMaintenanceException { - validateParameters(); - CertificateRequest request = createCertificateRequest(); - CertificateChoiceResponse response = fetchCertificateChoiceSessionResponse(request); - return response.getSessionID(); - } - - /** - * Create {@link SmartIdCertificate} from {@link SessionStatus} - *

      - * This method uses automatic session status polling internally - * and therefore blocks the current thread until certificate choice is concluded/interrupted etc. - * - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * - * @param sessionStatus session status response - * @return the authentication response - */ - public SmartIdCertificate createSmartIdCertificate(SessionStatus sessionStatus) { - validateCertificateResponse(sessionStatus); - SessionCertificate certificate = sessionStatus.getCert(); - SmartIdCertificate smartIdCertificate = new SmartIdCertificate(); - smartIdCertificate.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - smartIdCertificate.setCertificateLevel(certificate.getCertificateLevel()); - smartIdCertificate.setDocumentNumber(getDocumentNumber(sessionStatus)); - smartIdCertificate.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return smartIdCertificate; - } - - private CertificateChoiceResponse fetchCertificateChoiceSessionResponse(CertificateRequest request) { - if (isNotEmpty(getDocumentNumber())) { - return getConnector().getCertificate(getDocumentNumber(), request); - } - else if(getSemanticsIdentifier() != null) { - return getConnector().getCertificate(getSemanticsIdentifier(), request); - } - else { - throw new IllegalStateException("Either set semanticsIdentifier or documentNumber"); - } - } - - private CertificateRequest createCertificateRequest() { - CertificateRequest request = new CertificateRequest(); - request.setRelyingPartyUUID(getRelyingPartyUUID()); - request.setRelyingPartyName(getRelyingPartyName()); - request.setCertificateLevel(getCertificateLevel()); - request.setNonce(getNonce()); - request.setCapabilities(getCapabilities()); - - RequestProperties requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - if (requestProperties.hasProperties()) { - request.setRequestProperties(requestProperties); - } - - return request; - } - - public void validateCertificateResponse(SessionStatus sessionStatus) { - validateSessionResult(sessionStatus.getResult()); - SessionCertificate certificate = sessionStatus.getCert(); - if (certificate == null || isEmpty(certificate.getValue())) { - logger.error("Certificate was not present in the session status response"); - throw new UnprocessableSmartIdResponseException("Certificate was not present in the session status response"); - } - if (isEmpty(sessionStatus.getResult().getDocumentNumber())) { - logger.error("Document number was not present in the session status response"); - throw new UnprocessableSmartIdResponseException("Document number was not present in the session status response"); - } - } - - protected void validateParameters() { - super.validateParameters(); - } - - private String getDocumentNumber(SessionStatus sessionStatus) { - SessionResult sessionResult = sessionStatus.getResult(); - return sessionResult.getDocumentNumber(); - } -} diff --git a/src/main/java/ee/sk/smartid/v2/SignableData.java b/src/main/java/ee/sk/smartid/v2/SignableData.java deleted file mode 100644 index 02e636de..00000000 --- a/src/main/java/ee/sk/smartid/v2/SignableData.java +++ /dev/null @@ -1,89 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - -/** - * This class can be used to contain the data - * to be signed when it is not yet in hashed format - *

      - * {@link #setHashType(HashType)} can be used - * to set the wanted hash type. SHA-512 is default. - *

      - * {@link #calculateHash()} and - * {@link #calculateHashInBase64()} methods - * are used to calculate the hash for signing request. - *

      - * {@link SignableHash} can be used - * instead when the data to be signed is already - * in hashed format. - */ -public class SignableData implements Serializable { - - private final byte[] dataToSign; - private HashType hashType = HashType.SHA512; - - public SignableData(byte[] dataToSign) { - this.dataToSign = dataToSign.clone(); - } - - public String calculateHashInBase64() { - byte[] digest = calculateHash(); - return Base64.getEncoder().encodeToString(digest); - } - - public byte[] calculateHash() { - return DigestCalculator.calculateDigest(dataToSign, hashType); - } - - /** - * Calculates the verification code from the data - *

      - * Verification code should be displayed on the web page or some sort of web service - * so the person signing through the Smart-ID mobile app can verify if the verification code - * displayed on the phone matches with the one shown on the web page. - * - * @return the verification code - */ - public String calculateVerificationCode() { - byte[] digest = calculateHash(); - return VerificationCodeCalculator.calculate(digest); - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - - public HashType getHashType() { - return hashType; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/SignableHash.java b/src/main/java/ee/sk/smartid/v2/SignableHash.java deleted file mode 100644 index 2b6572c5..00000000 --- a/src/main/java/ee/sk/smartid/v2/SignableHash.java +++ /dev/null @@ -1,88 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.HashType; - -/** - * This class can be used to contain the hash - * to be signed - *

      - * {@link #setHash(byte[])} can be used - * to set the hash. - * {@link #setHashType(HashType)} can be used - * to set the hash type. - *

      - * {@link SignableData} can be used - * instead when the data to be signed is not already - * in hashed format. - */ -public class SignableHash implements Serializable { - - private byte[] hash; - private HashType hashType; - - public void setHash(byte[] hash) { - this.hash = hash.clone(); - } - - public void setHashInBase64(String hashInBase64) { - hash = Base64.getDecoder().decode(hashInBase64); - } - - public String getHashInBase64() { - return Base64.getEncoder().encodeToString(hash); - } - - public HashType getHashType() { - return hashType; - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - - public boolean areFieldsFilled() { - return hashType != null && hash != null && hash.length > 0; - } - - /** - * Calculates the verification code from the hash - *

      - * Verification code should be displayed on the web page or some sort of web service - * so the person signing through the Smart-ID mobile app can verify if the verification code - * displayed on the phone matches with the one shown on the web page. - * - * @return the verification code - */ - public String calculateVerificationCode() { - return VerificationCodeCalculator.calculate(hash); - } -} diff --git a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java deleted file mode 100644 index 3a100b5a..00000000 --- a/src/main/java/ee/sk/smartid/v2/SignatureRequestBuilder.java +++ /dev/null @@ -1,392 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionSignature; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnector; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import static ee.sk.smartid.util.StringUtil.isNotEmpty; - -/** - * Class for building signature request and getting the response - *

      - * Mandatory request parameters: - *

        - *
      • Host url - can be set on the {@link SmartIdClient} level
      • - *
      • Relying party uuid - can either be set on the client or builder level
      • - *
      • Relying party name - can either be set on the client or builder level
      • - *
      • Document number
      • - *
      • Either Signable hash or Signable data
      • - *
      - * Optional request parameters: - *
        - *
      • Certificate level
      • - *
      • Display text
      • - *
      • Nonce
      • - *
      - */ -public class SignatureRequestBuilder extends SmartIdRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(SignatureRequestBuilder.class); - - /** - * Constructs a new {@code SignatureRequestBuilder} - * - * @param connector for requesting signing initiation - * @param sessionStatusPoller for polling the signature response - */ - public SignatureRequestBuilder(SmartIdConnector connector, SessionStatusPoller sessionStatusPoller) { - super(connector, sessionStatusPoller); - logger.debug("Instantiating signature request builder"); - } - - /** - * Sets the request's UUID of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyUUID(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set the UUID every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - * @return this builder - */ - public SignatureRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the request's name of the relying party - *

      - * If not for explicit need, it is recommended to use - * {@link SmartIdClient#setRelyingPartyName(String)} - * instead. In that case when getting the builder from - * {@link SmartIdClient} it is not required - * to set name every time when building a new request. - * - * @param relyingPartyName name of the relying party - * @return this builder - */ - public SignatureRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the request's document number - *

      - * Document number is unique for the user's certificate/device - * that is used for the signing. - * - * @param documentNumber document number of the certificate/device used to sign - * @return this builder - */ - public SignatureRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, a hyphen and the identifier. - * - * @param semanticsIdentifierAsString semantics identifier for a person - * @return this builder - */ - public SignatureRequestBuilder withSemanticsIdentifierAsString(String semanticsIdentifierAsString) { - this.semanticsIdentifier = new SemanticsIdentifier(semanticsIdentifierAsString); - return this; - } - - /** - * Sets the request's personal semantics identifier - *

      - * Semantics identifier consists of identity type, country code, and the identifier. - * - * @param semanticsIdentifier semantics identifier for a person - * @return this builder - */ - public SignatureRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the data of the document to be signed - *

      - * This method could be used when the data - * to be signed is not in hashed format. - * {@link SignableData#setHashType(HashType)} - * can be used to select the wanted hash type - * and the data is hashed for you. - * - * @param dataToSign data to be signed - * @return this builder - */ - public SignatureRequestBuilder withSignableData(SignableData dataToSign) { - super.dataToSign = dataToSign; - return this; - } - - /** - * Sets the hash to be signed - *

      - * This method could be used when the data - * to be signed is in hashed format. - * - * @param hashToSign hash to be signed - * @return this builder - */ - public SignatureRequestBuilder withSignableHash(SignableHash hashToSign) { - super.hashToSign = hashToSign; - return this; - } - - /** - * Sets the request's certificate level - *

      - * Defines the minimum required level of the certificate. - * Optional. When not set, it defaults to what is configured - * on the server side i.e. "QUALIFIED". - * - * @param certificateLevel the level of the certificate - * @return this builder - */ - public SignatureRequestBuilder withCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the request's nonce - *

      - * By default, the signature's initiation request - * has idempotent behaviour meaning when the request - * is repeated inside a given time frame with exactly - * the same parameters, session ID of an existing session - * can be returned as a result. When requester wants, it can - * override the idempotent behaviour inside of this time frame - * using an optional "nonce" parameter present for all POST requests. - *

      - * Normally, this parameter can be omitted. - * - * @param nonce nonce of the request - * @return this builder - */ - public SignatureRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Specifies capabilities of the user - *

      - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(String...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of [QUALIFIED, ADVANCED] - * @return this builder - */ - public SignatureRequestBuilder withCapabilities(Capability... capabilities) { - this.capabilities = Arrays.stream(capabilities).map(Objects::toString).collect(Collectors.toSet()); - return this; - } - - /** - * Specifies capabilities of the user - *

      - * - * By default, there are no specified capabilities. - * The capabilities need to be specified in case of - * a restricted Smart ID user - * {@link #withCapabilities(Capability...)} - * @param capabilities are specified capabilities for a restricted Smart ID user - * and is one of ["QUALIFIED", "ADVANCED"] - * @return this builder - */ - public SignatureRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = new HashSet<>(Arrays.asList(capabilities)); - return this; - } - - /** - * Ask to return the IP address of the mobile device where Smart-ID app was running. - * @see Mobile Device IP sharing - * - * @return this builder - */ - public SignatureRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * @param allowedInteractionsOrder Preferred order of what dialog to present to user. What actually gets displayed depends on user's device and its software version. - * First option from this list that the device is capable of handling is displayed to the user. - * @return this builder - */ - public SignatureRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; - return this; - } - - /** - * Send the signature request and get the response - *

      - * This method uses automatic session status polling internally - * and therefore blocks the current thread until signing is concluded/interrupted etc. - * - * @throws UserAccountNotFoundException when the user account was not found - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason. - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return the signature response - */ - public SmartIdSignature sign() throws UserAccountNotFoundException, UserRefusedException, - UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException, ServerMaintenanceException { - validateParameters(); - String sessionId = initiateSigning(); - SessionStatus sessionStatus = getSessionStatusPoller().fetchFinalSessionStatus(sessionId); - return createSmartIdSignature(sessionStatus); - } - - /** - * Send the signature request and get the session ID - * - * @throws UserAccountNotFoundException when the user account was not found - * @throws ServerMaintenanceException when the server is under maintenance - * - * @return session ID - later to be used for manual session status polling - */ - public String initiateSigning() throws UserAccountNotFoundException, ServerMaintenanceException { - validateParameters(); - SignatureSessionRequest request = createSignatureSessionRequest(); - SignatureSessionResponse response = getSignatureResponse(request); - return response.getSessionID(); - } - - private SignatureSessionResponse getSignatureResponse(SignatureSessionRequest request) { - if (isNotEmpty(getDocumentNumber())) { - return getConnector().sign(getDocumentNumber(), request); - } - else { - return getConnector().sign(getSemanticsIdentifier(), request); - } - } - - /** - * Get {@link SmartIdSignature} from {@link SessionStatus} - * - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given timeframe - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * @throws UnprocessableSmartIdResponseException when session status response's result is missing, or it has some unknown value - * - * @param sessionStatus session status response - * @return the authentication response - */ - public SmartIdSignature createSmartIdSignature(SessionStatus sessionStatus) { - validateSignatureResponse(sessionStatus); - SessionSignature sessionSignature = sessionStatus.getSignature(); - - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64(sessionSignature.getValue()); - signature.setAlgorithmName(sessionSignature.getAlgorithm()); - signature.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); - signature.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - signature.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return signature; - } - - protected void validateParameters() { - super.validateParameters(); - super.validateAuthSignParameters(); - } - - private void validateSignatureResponse(SessionStatus sessionStatus) { - validateSessionResult(sessionStatus.getResult()); - if (sessionStatus.getSignature() == null) { - logger.error("Signature was not present in the response"); - throw new UnprocessableSmartIdResponseException("Signature was not present in the response"); - } - } - - private SignatureSessionRequest createSignatureSessionRequest() { - SignatureSessionRequest request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(getRelyingPartyUUID()); - request.setRelyingPartyName(getRelyingPartyName()); - request.setCertificateLevel(getCertificateLevel()); - request.setHashType(getHashTypeString()); - request.setHash(getHashInBase64()); - request.setNonce(getNonce()); - request.setCapabilities(getCapabilities()); - request.setAllowedInteractionsOrder(getAllowedInteractionsOrder()); - - RequestProperties requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - if (requestProperties.hasProperties()) { - request.setRequestProperties(requestProperties); - } - - return request; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java b/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java deleted file mode 100644 index 0c1c8601..00000000 --- a/src/main/java/ee/sk/smartid/v2/SmartIdAuthenticationResponse.java +++ /dev/null @@ -1,153 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -import java.io.Serializable; -import java.security.cert.X509Certificate; -import java.util.Base64; - -public class SmartIdAuthenticationResponse implements Serializable { - - private String endResult; - private String signedHashInBase64; - private HashType hashType; - private String signatureValueInBase64; - private String algorithmName; - private X509Certificate certificate; - private String requestedCertificateLevel; - private String certificateLevel; - private String documentNumber; - private String interactionFlowUsed; - private String deviceIpAddress; - - public byte[] getSignatureValue() { - try { - return Base64.getDecoder().decode(signatureValueInBase64); - } - catch (IllegalArgumentException ie) { - throw new UnprocessableSmartIdResponseException("Failed to parse signature value in base64. Probably incorrectly encoded base64 string: '" + signatureValueInBase64); - } - } - - public String getEndResult() { - return endResult; - } - - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - public String getSignatureValueInBase64() { - return signatureValueInBase64; - } - - public void setSignatureValueInBase64(String signatureValueInBase64) { - this.signatureValueInBase64 = signatureValueInBase64; - } - - public String getAlgorithmName() { - return algorithmName; - } - - public void setAlgorithmName(String algorithmName) { - this.algorithmName = algorithmName; - } - - public X509Certificate getCertificate() { - return certificate; - } - - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getSignedHashInBase64() { - return signedHashInBase64; - } - - public void setSignedHashInBase64(String signedHashInBase64) { - this.signedHashInBase64 = signedHashInBase64; - } - - public HashType getHashType() { - return hashType; - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - - public String getRequestedCertificateLevel() { - return requestedCertificateLevel; - } - - public void setRequestedCertificateLevel(String requestedCertificateLevel) { - this.requestedCertificateLevel = requestedCertificateLevel; - } - - public String getDocumentNumber() { - return documentNumber; - } - - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * IP address of the device running the App. - * Present only for subscribed RPs and when available (e.g. not present in case state is TIMEOUT). - * - * @return IP address of the device running Smart-id app (or null if not returned) - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java b/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java deleted file mode 100644 index b719b96d..00000000 --- a/src/main/java/ee/sk/smartid/v2/SmartIdCertificate.java +++ /dev/null @@ -1,71 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.security.cert.X509Certificate; - -public class SmartIdCertificate implements Serializable { - - private X509Certificate certificate; - private String documentNumber; - private String certificateLevel; - private String deviceIpAddress; - - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - public X509Certificate getCertificate() { - return certificate; - } - - public String getDocumentNumber() { - return documentNumber; - } - - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - - public String getDeviceIpAddress() { - return deviceIpAddress; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdClient.java b/src/main/java/ee/sk/smartid/v2/SmartIdClient.java deleted file mode 100644 index bdd726e5..00000000 --- a/src/main/java/ee/sk/smartid/v2/SmartIdClient.java +++ /dev/null @@ -1,291 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.SmartIdRestConnector; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.core.Configuration; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class SmartIdClient { - - private String relyingPartyUUID; - private String relyingPartyName; - private String hostUrl; - private Configuration networkConnectionConfig; - private Client configuredClient; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - private SmartIdConnector connector; - private SSLContext trustSslContext; - - /** - * Gets an instance of the certificate request builder - * - * @return certificate request builder instance - */ - public CertificateRequestBuilder getCertificate() { - SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - var builder = new CertificateRequestBuilder(getSmartIdConnector(), sessionStatusPoller); - builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); - builder.withRelyingPartyName(this.getRelyingPartyName()); - return builder; - } - - /** - * Gets an instance of the signature request builder - * - * @return signature request builder instance - */ - public SignatureRequestBuilder createSignature() { - SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - var builder = new SignatureRequestBuilder(getSmartIdConnector(), sessionStatusPoller); - builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); - builder.withRelyingPartyName(this.getRelyingPartyName()); - return builder; - } - - /** - * Gets an instance of the authentication request builder - * - * @return authentication request builder instance - */ - public AuthenticationRequestBuilder createAuthentication() { - SessionStatusPoller sessionStatusPoller = createSessionStatusPoller(getSmartIdConnector()); - var builder = new AuthenticationRequestBuilder(getSmartIdConnector(), sessionStatusPoller); - builder.withRelyingPartyUUID(this.getRelyingPartyUUID()); - builder.withRelyingPartyName(this.getRelyingPartyName()); - return builder; - } - - /** - * Sets the UUID of the relying party - *

      - * Can be set also on the builder level, - * but in that case it has to be set explicitly - * every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - */ - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - /** - * Gets the UUID of the relying party - * - * @return UUID of the relying party - */ - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - /** - * Sets the name of the relying party - *

      - * Can be set also on the builder level, - * but in that case it has to be set - * every time when building a new request. - * - * @param relyingPartyName name of the relying party - */ - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - /** - * Gets the name of the relying party - * - * @return name of the relying party - */ - public String getRelyingPartyName() { - return relyingPartyName; - } - - /** - * Sets the base URL of the Smart-ID backend environment - *

      - * It defines the endpoint which the client communicates to. - * - * @param hostUrl base URL of the Smart-ID backend environment - */ - public void setHostUrl(String hostUrl) { - this.hostUrl = hostUrl; - } - - /** - * Sets the network connection configuration - *

      - * Useful for configuring network connection - * timeouts, proxy settings, request headers etc. - * - * @param networkConnectionConfig Jersey's network connection configuration instance - */ - public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { - this.networkConnectionConfig = networkConnectionConfig; - } - - public void setConfiguredClient(Client configuredClient) { - this.configuredClient = configuredClient; - } - - /** - * Sets the timeout for each session status poll - *

      - * Under the hood each operation (authentication, signing, choosing - * certificate) consists of 2 request steps: - *

      - * 1. Initiation request - *

      - * 2. Session status request - *

      - * Session status request is a long poll method, meaning - * the request method might not return until a timeout expires - * set by this parameter. - *

      - * Caller can tune the request parameters inside the bounds - * set by service operator. - *

      - * If not provided, a default is used. - * - * @param timeUnit time unit of the {@code timeValue} argument - * @param timeValue time value of each status poll's timeout. - */ - public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - sessionStatusResponseSocketOpenTimeUnit = timeUnit; - sessionStatusResponseSocketOpenTimeValue = timeValue; - } - - /** - * Sets the timeout/pause between each session status poll - * - * @param unit time unit of the {@code timeout} argument - * @param timeout timeout value in the given {@code unit} - */ - public void setPollingSleepTimeout(TimeUnit unit, long timeout) { - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; - } - - private SessionStatusPoller createSessionStatusPoller(SmartIdConnector connector) { - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - var sessionStatusPoller = new SessionStatusPoller(connector); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); - return sessionStatusPoller; - } - - public SmartIdConnector getSmartIdConnector() { - if (null == connector) { - // Fallback to REST connector when not initialised - SmartIdRestConnector connector = configuredClient != null ? new SmartIdRestConnector(hostUrl, configuredClient) : new SmartIdRestConnector(hostUrl, networkConnectionConfig); - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - - if (trustSslContext == null && configuredClient == null) { - throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); - } - - connector.setSslContext(this.trustSslContext); - setSmartIdConnector(connector); - } - return connector; - } - - public static SSLContext createSslContext(List sslCertificates) - throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - CertificateFactory factory = CertificateFactory.getInstance("X509"); - int i = 0; - for (String sslCertificate : sslCertificates) { - Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); - keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); - } - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(keyStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext; - } - - public void setTrustSslContext(SSLContext trustSslContext) { - this.trustSslContext = trustSslContext; - } - - public void setTrustStore(KeyStore trustStore) { - try { - SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - var trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(trustStore); - trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); - this.trustSslContext = trustSslContext; - } - catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); - } - } - - public void setTrustedCertificates(String ...sslCertificates) { - try { - this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); - } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Failed to createSslContext", e); - } - } - - public void setSmartIdConnector(SmartIdConnector smartIdConnector) { - this.connector = smartIdConnector; - } - - /** Use setTrustStore() instead - * @param trustStore Trust store to load certificates from. - */ - @Deprecated - public void loadSslCertificatesFromKeystore(KeyStore trustStore) { - this.setTrustStore(trustStore); - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java b/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java deleted file mode 100644 index 84f1b68b..00000000 --- a/src/main/java/ee/sk/smartid/v2/SmartIdRequestBuilder.java +++ /dev/null @@ -1,235 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionResult; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnector; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Set; - -import static ee.sk.smartid.util.StringUtil.isEmpty; - -public abstract class SmartIdRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(SmartIdRequestBuilder.class); - - private final SmartIdConnector connector; - private final SessionStatusPoller sessionStatusPoller; - protected String relyingPartyUUID; - protected String relyingPartyName; - protected SemanticsIdentifier semanticsIdentifier; - - protected String documentNumber; - protected String certificateLevel; - protected SignableData dataToSign; - protected SignableHash hashToSign; - protected String nonce; - protected Set capabilities; - protected List allowedInteractionsOrder; - protected Boolean shareMdClientIpAddress; - - protected SmartIdRequestBuilder(SmartIdConnector connector, SessionStatusPoller sessionStatusPoller) { - this.connector = connector; - this.sessionStatusPoller = sessionStatusPoller; - } - - protected void validateParameters() { - if (isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); - } - if (isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); - } - if (nonce != null && nonce.length() > 30) { - throw new SmartIdClientException("Nonce cannot be longer that 30 chars. You supplied: '" + nonce + "'"); - } - - int identifierCount = getIdentifiersCount(); - - if (identifierCount == 0) { - logger.error("Either documentNumber or semanticsIdentifier must be set"); - throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set"); - } - else if (identifierCount > 1 ) { - logger.error("Exactly one of documentNumber or semanticsIdentifier must be set"); - throw new SmartIdClientException("Exactly one of documentNumber or semanticsIdentifier must be set"); - } - } - - protected void validateAuthSignParameters() { - if (!isHashSet() && !isSignableDataSet()) { - logger.error("Either dataToSign or hash with hashType must be set"); - throw new SmartIdClientException("Either dataToSign or hash with hashType must be set"); - } - validateAllowedInteractionOrder(); - } - - private void validateAllowedInteractionOrder() { - if (getAllowedInteractionsOrder() == null || getAllowedInteractionsOrder().isEmpty()) { - logger.error("Missing or empty mandatory parameter allowedInteractionsOrder"); - throw new SmartIdClientException("Missing or empty mandatory parameter allowedInteractionsOrder"); - } - getAllowedInteractionsOrder().forEach(Interaction::validate); - } - - private int getIdentifiersCount() { - int identifierCount = 0; - if (!isEmpty(getDocumentNumber())) { - identifierCount++; - } - if (hasSemanticsIdentifier()) { - identifierCount++; - } - return identifierCount; - } - - protected void validateSessionResult(SessionResult result) { - if (result == null) { - logger.error("Result is missing in the session status response"); - throw new UnprocessableSmartIdResponseException("Result is missing in the session status response"); - } - String endResult = result.getEndResult().toUpperCase(); - - logger.debug("Smart-ID end result code is '{}' ", endResult); - - switch (endResult) { - case "OK": - return; - case "USER_REFUSED": - throw new UserRefusedException(); - case "TIMEOUT": - throw new SessionTimeoutException(); - case "DOCUMENT_UNUSABLE": - throw new DocumentUnusableException(); - case "WRONG_VC": - throw new UserSelectedWrongVerificationCodeException(); - case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP": - throw new RequiredInteractionNotSupportedByAppException(); - case "USER_REFUSED_CERT_CHOICE": - throw new UserRefusedCertChoiceException(); - case "USER_REFUSED_DISPLAYTEXTANDPIN": - throw new UserRefusedDisplayTextAndPinException(); - case "USER_REFUSED_VC_CHOICE": - throw new UserRefusedVerificationChoiceException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE": - throw new UserRefusedConfirmationMessageException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE": - throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); - default: - throw new UnprocessableSmartIdResponseException("Session status end result is '" + endResult + "'"); - } - } - - protected boolean hasSemanticsIdentifier() { - return semanticsIdentifier != null; - } - - protected boolean isHashSet() { - return hashToSign != null && hashToSign.areFieldsFilled(); - } - - protected boolean isSignableDataSet() { - return dataToSign != null; - } - - protected String getHashTypeString() { - return getHashType().getHashTypeName(); - } - - protected HashType getHashType() { - if (hashToSign != null) { - return hashToSign.getHashType(); - } - return dataToSign.getHashType(); - } - - protected String getHashInBase64() { - if (hashToSign != null) { - return hashToSign.getHashInBase64(); - } - return dataToSign.calculateHashInBase64(); - } - - public SmartIdConnector getConnector() { - return connector; - } - - protected SessionStatusPoller getSessionStatusPoller() { - return sessionStatusPoller; - } - - protected String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - protected String getRelyingPartyName() { - return relyingPartyName; - } - - protected String getDocumentNumber() { - return documentNumber; - } - - protected String getCertificateLevel() { - return certificateLevel; - } - - protected String getNonce() { - return nonce; - } - - public SemanticsIdentifier getSemanticsIdentifier() { return semanticsIdentifier; } - - public Set getCapabilities() { return capabilities; } - - public List getAllowedInteractionsOrder() { - return allowedInteractionsOrder; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java b/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java deleted file mode 100644 index 15ed7721..00000000 --- a/src/main/java/ee/sk/smartid/v2/SmartIdSignature.java +++ /dev/null @@ -1,97 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -import java.io.Serializable; -import java.util.Base64; - -public class SmartIdSignature implements Serializable { - - private String valueInBase64; - private String algorithmName; - private String documentNumber; - private String interactionFlowUsed; - private String deviceIpAddress; - - public byte[] getValue() { - try { - return Base64.getDecoder().decode(valueInBase64); - } - catch (IllegalArgumentException ie) { - throw new UnprocessableSmartIdResponseException("Failed to parse signature value in base64. Probably incorrectly encoded base64 string: '" + valueInBase64); - } - } - - public String getValueInBase64() { - return valueInBase64; - } - - public void setValueInBase64(String valueInBase64) { - this.valueInBase64 = valueInBase64; - } - - public String getAlgorithmName() { - return algorithmName; - } - - public void setAlgorithmName(String algorithmName) { - this.algorithmName = algorithmName; - } - - public String getDocumentNumber() { - return documentNumber; - } - - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * IP address of the device running the App. - * Present only for subscribed RPs and when available (e.g. not present in case state is TIMEOUT). - * - * @return IP address of the device running Smart-id app (or null if not returned) - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java deleted file mode 100644 index 1156f6ab..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/SessionStatusPoller.java +++ /dev/null @@ -1,87 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -public class SessionStatusPoller { - - private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - - private final SmartIdConnector connector; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - - public SessionStatusPoller(SmartIdConnector connector) { - this.connector = connector; - } - - public SessionStatus fetchFinalSessionStatus(String sessionId) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - logger.debug("Starting to poll session status for session " + sessionId); - try { - return pollForFinalSessionStatus(sessionId); - } catch (InterruptedException e) { - logger.error("Failed to poll session status: " + e.getMessage()); - throw new UnprocessableSmartIdResponseException("Failed to poll session status: " + e.getMessage(), e); - } - } - - private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { - SessionStatus sessionStatus = null; - while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState()) ) { - sessionStatus = pollSessionStatus(sessionId); - if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState()) ) { - break; - } - logger.debug("Sleeping for " + pollingSleepTimeout + " " + pollingSleepTimeUnit); - pollingSleepTimeUnit.sleep(pollingSleepTimeout); - } - logger.debug("Got session final session status response"); - return sessionStatus; - } - - private SessionStatus pollSessionStatus(String sessionId) { - logger.debug("Polling session status"); - return connector.getSessionStatus(sessionId); - } - - public void setPollingSleepTime(TimeUnit unit, long timeout) { - logger.debug("Polling sleep time is " + timeout + " " + unit.toString()); - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java deleted file mode 100644 index cc23742e..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdConnector.java +++ /dev/null @@ -1,63 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -import javax.net.ssl.SSLContext; -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -public interface SmartIdConnector extends Serializable { - - SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; - - CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request); - - CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, CertificateRequest request); - - SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request); - - SignatureSessionResponse sign(SemanticsIdentifier identifier, SignatureSessionRequest request); - - AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request); - - AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, AuthenticationSessionRequest request); - - void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); - - void setSslContext(SSLContext sslContext); - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java deleted file mode 100644 index 3246be2f..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/SmartIdRestConnector.java +++ /dev/null @@ -1,327 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.LoggingFilter; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SessionStatusRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; -import jakarta.ws.rs.*; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.UriBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.ssl.SSLContext; - -import java.io.Serial; -import java.net.URI; -import java.util.concurrent.TimeUnit; - -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; - -public class SmartIdRestConnector implements SmartIdConnector { - - @Serial - private static final long serialVersionUID = 43L; - - private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); - - private static final String SESSION_STATUS_URI = "/session/{sessionId}"; - - private static final String CERTIFICATE_CHOICE_BY_DOCUMENT_NUMBER_PATH = "/certificatechoice/document/{documentNumber}"; - private static final String CERTIFICATE_CHOICE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER = "/certificatechoice/etsi/{semanticsIdentifier}"; - - private static final String SIGNATURE_BY_DOCUMENT_NUMBER_PATH = "/signature/document/{documentNumber}"; - private static final String SIGNATURE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER = "/signature/etsi/{semanticsIdentifier}"; - - private static final String AUTHENTICATE_BY_DOCUMENT_NUMBER_PATH = "/authentication/document/{documentNumber}"; - private static final String AUTHENTICATE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER = "/authentication/etsi/{semanticsIdentifier}"; - - private final String endpointUrl; - private transient Configuration clientConfig; - private transient Client configuredClient; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - private transient SSLContext sslContext; - - public SmartIdRestConnector(String endpointUrl) { - this.endpointUrl = endpointUrl; - } - - public SmartIdRestConnector(String endpointUrl, Configuration clientConfig) { - this(endpointUrl); - this.clientConfig = clientConfig; - } - - public SmartIdRestConnector(String endpointUrl, Client configuredClient) { - this(endpointUrl); - this.configuredClient = configuredClient; - } - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - logger.debug("Getting session status for " + sessionId); - SessionStatusRequest request = createSessionStatusRequest(sessionId); - UriBuilder uriBuilder = UriBuilder - .fromUri(endpointUrl) - .path(SESSION_STATUS_URI); - addResponseSocketOpenTimeUrlParameter(request, uriBuilder); - URI uri = uriBuilder.build(request.getSessionId()); - try { - return prepareClient(uri).get(SessionStatus.class); - } catch (NotFoundException e) { - logger.warn("Session " + request + " not found: " + e.getMessage()); - throw new SessionNotFoundException(); - } - } - - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - logger.debug("Getting certificate for document " + documentNumber); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(CERTIFICATE_CHOICE_BY_DOCUMENT_NUMBER_PATH) - .build(documentNumber); - return postCertificateRequest(uri, request); - } - - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier semanticsIdentifier, - CertificateRequest request) { - logger.debug("Getting certificate for identifier " + semanticsIdentifier.getIdentifier()); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(CERTIFICATE_CHOICE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER) - .build(semanticsIdentifier.getIdentifier()); - return postCertificateRequest(uri, request); - } - - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - logger.debug("Signing for document " + documentNumber); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(SIGNATURE_BY_DOCUMENT_NUMBER_PATH) - .build(documentNumber); - - return postSigningRequest(uri, request); - } - - @Override - public SignatureSessionResponse sign(SemanticsIdentifier semanticsIdentifier, SignatureSessionRequest request) { - logger.debug("Signing for " + semanticsIdentifier); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(SIGNATURE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER) - .build(semanticsIdentifier.getIdentifier()); - - return postSigningRequest(uri, request); - } - - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - logger.debug("Authenticating for document " + documentNumber); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(AUTHENTICATE_BY_DOCUMENT_NUMBER_PATH) - .build(documentNumber); - return postAuthenticationRequest(uri, request); - } - - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier semanticsIdentifier, AuthenticationSessionRequest request) { - logger.debug("Authenticating for " + semanticsIdentifier); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(AUTHENTICATE_BY_NATURAL_PERSON_SEMANTICS_IDENTIFIER) - .build(semanticsIdentifier.getIdentifier()); - return postAuthenticationRequest(uri, request); - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } - - protected Invocation.Builder prepareClient(URI uri) { - Client client; - if (this.configuredClient == null) { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (null != this.clientConfig) { - clientBuilder.withConfig(this.clientConfig); - } - if (null != this.sslContext) { - clientBuilder.sslContext(this.sslContext); - } - client = clientBuilder.build(); - } - else { - client = this.configuredClient; - } - - return client - .register(new LoggingFilter()) - .target(uri) - .request() - .accept(APPLICATION_JSON_TYPE) - .header("User-Agent", buildUserAgentString()); - } - - protected String buildUserAgentString() { - return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; - } - - protected String getClientVersion() { - String clientVersion = getClass().getPackage().getImplementationVersion(); - return clientVersion == null ? "-" : clientVersion; - } - - protected String getJdkMajorVersion() { - try { - return System.getProperty("java.version").split("_")[0]; - } - catch (Exception e) { - return "-"; - } - } - - private CertificateChoiceResponse postCertificateRequest(URI uri, CertificateRequest request) { - try { - return postRequest(uri, request, CertificateChoiceResponse.class); - } catch (NotFoundException e) { - logger.warn("Certificate not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } - - private AuthenticationSessionResponse postAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { - try { - return postRequest(uri, request, AuthenticationSessionResponse.class); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } - - private SignatureSessionResponse postSigningRequest(URI uri, SignatureSessionRequest request) { - try { - return postRequest(uri, request, SignatureSessionResponse.class); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } - - private T postRequest(URI uri, V request, Class responseType) { - try { - Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); - return prepareClient(uri).post(requestEntity, responseType); - } - catch (NotAuthorizedException e) { - logger.warn("Request is unauthorized for URI " + uri, e); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, e); - } - catch (BadRequestException e) { - logger.warn("Request is invalid for URI " + uri, e); - throw new SmartIdClientException("Server refused the request", e); - } - catch (ClientErrorException e) { - if (e.getResponse().getStatus() == 471) { - logger.warn("No suitable account of requested type found, but user has some other accounts.", e); - throw new NoSuitableAccountOfRequestedTypeFoundException(); - } - if (e.getResponse().getStatus() == 472) { - logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", e); - throw new PersonShouldViewSmartIdPortalException(); - } - if (e.getResponse().getStatus() == 480) { - logger.warn("Client-side API is too old and not supported anymore"); - throw new SmartIdClientException("Client-side API is too old and not supported anymore"); - } - throw e; - } - catch (ServerErrorException e) { - if (e.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", e); - throw new ServerMaintenanceException(); - } - throw e; - } - } - - - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - SessionStatusRequest request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; - } - - private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { - if (request.isResponseSocketOpenTimeSet()) { - TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); - long timeValue = request.getResponseSocketOpenTimeValue(); - long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); - uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); - } - } - - @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java deleted file mode 100644 index 8e336879..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionRequest.java +++ /dev/null @@ -1,136 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.List; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -public class AuthenticationSessionRequest implements Serializable { - - private String relyingPartyUUID; - - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - private String hash; - - private String hashType; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private List allowedInteractionsOrder; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getHash() { - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - - public String getHashType() { - return hashType; - } - - public void setHashType(String hashType) { - this.hashType = hashType; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - private void setDisplayText(String displayText) { - throw new UnsupportedOperationException("Method is removed in Smart-ID API 2.0 and replaced with setAllowedInteractionsOrder()"); - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public List getAllowedInteractionsOrder() { - return allowedInteractionsOrder; - } - - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java deleted file mode 100644 index 3dfab7a5..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/AuthenticationSessionResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class AuthenticationSessionResponse implements Serializable { - - private String sessionID; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java deleted file mode 100644 index c141610f..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/Capability.java +++ /dev/null @@ -1,31 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -public enum Capability { - QUALIFIED, ADVANCED -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java deleted file mode 100644 index 13b3b785..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateChoiceResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class CertificateChoiceResponse implements Serializable { - - private String sessionID; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java deleted file mode 100644 index 9d5a19a7..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/CertificateRequest.java +++ /dev/null @@ -1,100 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; -import java.util.Set; - -public class CertificateRequest implements Serializable { - - private String relyingPartyUUID; - - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java deleted file mode 100644 index c5a0fde1..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/Interaction.java +++ /dev/null @@ -1,132 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.v2.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.v2.rest.dao.InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; -import static ee.sk.smartid.v2.rest.dao.InteractionFlow.DISPLAY_TEXT_AND_PIN; -import static ee.sk.smartid.v2.rest.dao.InteractionFlow.VERIFICATION_CODE_CHOICE; - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class Interaction implements Serializable { - - private InteractionFlow type; - - private String displayText60; - private String displayText200; - - private Interaction(InteractionFlow type) { - this.type = type; - } - - public static Interaction displayTextAndPIN(String displayText60) { - Interaction interaction = new Interaction(DISPLAY_TEXT_AND_PIN); - interaction.displayText60 = displayText60; - return interaction; - } - - public static Interaction verificationCodeChoice(String displayText60) { - Interaction interaction = new Interaction(VERIFICATION_CODE_CHOICE); - interaction.displayText60 = displayText60; - return interaction; - } - - public static Interaction confirmationMessage(String displayText200) { - Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE); - interaction.displayText200 = displayText200; - return interaction; - } - - public static Interaction confirmationMessageAndVerificationCodeChoice(String displayText200) { - Interaction interaction = new Interaction(InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - interaction.displayText200 = displayText200; - return interaction; - } - - public InteractionFlow getType() { - return type; - } - - public void setType(InteractionFlow type) { - this.type = type; - } - - public String getDisplayText60() { - return displayText60; - } - - public void setDisplayText60(String displayText60) { - this.displayText60 = displayText60; - } - - public String getDisplayText200() { - return displayText200; - } - - public void setDisplayText200(String displayText200) { - this.displayText200 = displayText200; - } - - public void validate() { - validateDisplayText60(); - validateDisplayText200(); - } - - private void validateDisplayText60() { - if (getType() == VERIFICATION_CODE_CHOICE || getType() == DISPLAY_TEXT_AND_PIN) { - if (getDisplayText60() == null) { - throw new SmartIdClientException("displayText60 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText60().length() > 60) { - throw new SmartIdClientException("displayText60 must not be longer than 60 characters"); - } - if (getDisplayText200() != null) { - throw new SmartIdClientException("displayText200 must be null for AllowedInteractionOrder of type " + getType()); - } - } - } - - private void validateDisplayText200() { - if (getType() == CONFIRMATION_MESSAGE || getType() == CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { - if (getDisplayText200() == null) { - throw new SmartIdClientException("displayText200 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText200().length() > 200) { - throw new SmartIdClientException("displayText200 must not be longer than 200 characters"); - } - if (getDisplayText60() != null) { - throw new SmartIdClientException("displayText60 must be null for AllowedInteractionOrder of type " + getType()); - } - } - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java deleted file mode 100644 index 156f98ac..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/InteractionFlow.java +++ /dev/null @@ -1,53 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonValue; - -public enum InteractionFlow { - - DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), - CONFIRMATION_MESSAGE("confirmationMessage"), - VERIFICATION_CODE_CHOICE("verificationCodeChoice"), - CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); - - private final String code; - - InteractionFlow(String code) { - this.code = code; - } - - @JsonValue - public String getCode() { - return code; - } - - public boolean is(String typeCodeString) { - return this.getCode().equals(typeCodeString); - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java deleted file mode 100644 index d771e2a3..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/RequestProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; - -public class RequestProperties implements Serializable { - - @JsonInclude(JsonInclude.Include.NON_NULL) - Boolean shareMdClientIpAddress; - - public Boolean getShareMdClientIpAddress() { - return shareMdClientIpAddress; - } - - public void setShareMdClientIpAddress(Boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - } - - @JsonIgnore - public boolean hasProperties() { - return shareMdClientIpAddress != null; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java deleted file mode 100644 index cd64983e..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionCertificate.java +++ /dev/null @@ -1,54 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionCertificate implements Serializable { - - private String value; - private String certificateLevel; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java deleted file mode 100644 index 6c4e1faa..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionResult.java +++ /dev/null @@ -1,54 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionResult implements Serializable { - - private String endResult; - private String documentNumber; - - public String getDocumentNumber() { - return documentNumber; - } - - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getEndResult() { - return endResult; - } - - public void setEndResult(String endResult) { - this.endResult = endResult; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java deleted file mode 100644 index ec31ea7d..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionSignature.java +++ /dev/null @@ -1,55 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionSignature implements Serializable { - - private String algorithm; - - private String value; - - public String getAlgorithm() { - return algorithm; - } - - public void setAlgorithm(String algorithm) { - this.algorithm = algorithm; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java deleted file mode 100644 index b0518f91..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java +++ /dev/null @@ -1,113 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; -import java.util.Arrays; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionStatus implements Serializable { - - private String state; - private SessionResult result; - private SessionSignature signature; - - private SessionCertificate cert; - private String[] ignoredProperties = {}; - - private String interactionFlowUsed; - private String deviceIpAddress; - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public SessionResult getResult() { - return result; - } - - public void setResult(SessionResult result) { - this.result = result; - } - - public SessionCertificate getCert() { - return cert; - } - - public void setCert(SessionCertificate cert) { - this.cert = cert; - } - - public SessionSignature getSignature() { - return signature; - } - - public void setSignature(SessionSignature signature) { - this.signature = signature; - } - - public String[] getIgnoredProperties() { - return Arrays.copyOf(ignoredProperties, ignoredProperties.length); - } - - public void setIgnoredProperties(String[] ignoredProperties) { - this.ignoredProperties = Arrays.copyOf(ignoredProperties, ignoredProperties.length); - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * IP-address of the device running the App. - *

      - * Present only if withShareMdClientIpAddress() was specified with the request - * Also, the RelyingParty must be subscribed for the service. - * Also, the data must be available (e.g. not present in case state is TIMEOUT). - * @see Mobile Device IP sharing - * - * @return IP address of the device running Smart-ID app (or null if not returned) - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java deleted file mode 100644 index 9084eed8..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatusRequest.java +++ /dev/null @@ -1,73 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -public class SessionStatusRequest implements Serializable { - - private final String sessionId; - private TimeUnit responseSocketOpenTimeUnit; - private long responseSocketOpenTimeValue; - - public SessionStatusRequest(String sessionId) { - this.sessionId = sessionId; - } - - public String getSessionId() { - return sessionId; - } - - /** - * Request long poll timeout value. If not provided, a default is used. - *

      - * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires - * set by this parameter. - *

      - * Caller can tune the request parameters inside the bounds set by service operator. - * - * @param timeUnit time unit of how much time a network request socket should be kept open. - * @param timeValue time value of how much time a network request socket should be kept open. - */ - public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - responseSocketOpenTimeUnit = timeUnit; - responseSocketOpenTimeValue = timeValue; - } - - public boolean isResponseSocketOpenTimeSet() { - return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; - } - - public TimeUnit getResponseSocketOpenTimeUnit() { - return responseSocketOpenTimeUnit; - } - - public long getResponseSocketOpenTimeValue() { - return responseSocketOpenTimeValue; - } -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java deleted file mode 100644 index 473da6ab..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequest.java +++ /dev/null @@ -1,136 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; -import java.util.List; -import java.util.Set; - -public class SignatureSessionRequest implements Serializable { - - private String relyingPartyUUID; - - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - private String hash; - - private String hashType; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private List allowedInteractionsOrder; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getHash() { - return hash; - } - - public void setHash(String hash) { - this.hash = hash; - } - - public String getHashType() { - return hashType; - } - - public void setHashType(String hashType) { - this.hashType = hashType; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - protected void setDisplayText(String displayText) { - throw new UnsupportedOperationException("Method is removed in Smart-ID API 2.0 and replaced with setAllowedInteractionsOrder()"); - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public List getAllowedInteractionsOrder() { - return allowedInteractionsOrder; - } - - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - -} diff --git a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java b/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java deleted file mode 100644 index 2dc3a198..00000000 --- a/src/main/java/ee/sk/smartid/v2/rest/dao/SignatureSessionResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.io.Serializable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class SignatureSessionResponse implements Serializable { - - private String sessionID; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } -} diff --git a/src/main/resources/trusted_certificates/EID_NQ_2021E.der.crt b/src/main/resources/trusted_certificates/EID_NQ_2021E.der.crt new file mode 100644 index 0000000000000000000000000000000000000000..2a28306c2bb36eb8d49dcd534e331e1aa9a06cd5 GIT binary patch literal 912 zcmXqLV(u|$VtTuPnTe5!Ng!2EbF!@EzcqJkHs&Ff$vZ8FCwN zvN4CUun9A{x*AFwNP;+AJVL?V3Z55MW~mhX4~J8>@CB3xg7K5(5i^^7^R%S9P7{ zZu6Rxc=d3Oc2%wdx3Tc)p2aM?SC7c^A@OLsN7FSPY=~$Z{T=h6*v4x++;wHu@gC@o>aA3(Q zvq%_-HHfs8_*=cU__Sg9ySF=AXU}csbT!O2kOlfemXAe@MI`;ga~@f%i~G*CY@EY! z@vV=<$3wCPd?0ClM#ldvEWj9MGY|stg+Y7{12!PV#K>Tf%f_M2#>mRb&dA7;Xb^9p z0OK1lwuxkvloVL$>*psI7w8pd>!qgZJ3AU!!&NY~=|fc{L#2bg<2_yC;fWz06s_^V zXw^$eEz(OaDlyOpIYFL9!$8eIWr5NHg*G`%6X2SXi*gJCKpGTSJPq6pT-jKFOa=p6 zgcFz;#lTJ|2D-Amyd3C?%#>nSQq)T=)lbdSFG?-QFU~B*#6}FpG|?1xfW+dLv)@Vur$23%sqEqGHcmyf%hz1&eq&E za&S;w@n6vLrgLWg@*_We{JCB<0nLzBQ93L%Yp-4N2K`y5Wi;R9KfC37P~?(y z6kU~i6sgElx*g|8xpg6Rbo59`ZZVv#Q=Oj9zvr*-_x*i;Ki}u|e4o$z17zqWAVUg^ zurLI}y3>QuAZtaiA~;|mpi*+T6A)0X1@e{h5ds9^H83EIIvr@CST=&{!%QaC1Q@FY z4zI`ZBziE2tf(#GnDD4b5z&oBH3YNNW-wmc%g>j|BvXMC-3d?~sFuJ&ZD8?6-&gsr zUF%15r}_eOLjsLT1%OVa(HtD<0fq#JAHI$$nfSg$vB9BX6a9M=QI zaB;^?x9jQK!yk>^k4<6*vhBz@T^BQ7wdML;4nB4pajo8GLr6_1awqqYg%hKh&ekg) zM_+YXpSUUVQE5w;nSU*APISxGQsd!@gd39+!%N$87+z4+56;t1X%|zsX($SlG&$E*eCRGtL&6>3!AAQ z*7JTdel@Q#57dWmFRMZn$31W_EV_n(zv(VQjz+PTa}rIL+zeVUi#VjBB`>sbF|`Z(ETXPaCw*6^C*m`-Dm zpWNP8lm9X31#MaUKvj#X;lNf@ z(-ws^G^Rli5)3$i_(cOqK9e646TO&1i3$}(+lvJDJRW7a8(8x71}vw1z7hJ_W_fZv z7@V)s<)}l*QH9W+%M;p%3S$68jbMSy12(|A$f}4aH~;SoUyDM8TY$H^fQWbicfdpu z)fNMs|3!cygHH$|6=iH}tcoI>EBZ1x?0I4eFOnkUMMsIkW1@s{l;z$m%D4GbjUiS3 zAk1mf<|icwlAHdFD>)~oZ6r`zPUyOZG?nij+B`7QwB>5(Ux zj9h`-(%jD*J+6|T>UWB)Rl zCrTZ77vn>~rAp=&WahQeGTu&yi|S4hPeToP6MeZiIR@nt5t1!FU!L8wVcUxSwpzY* z^Igx;<7YP@MX|psgtf!eoPxa#T`P@GnuM&mcdO&rK^LR)yu6gw3nmT5#ghRZ5=?I{>2R#sB%159;oK~LC$Evgc|5nriUEs*KD<+Z1N3X9 Avj6}9 literal 0 HcmV?d00001 diff --git a/src/main/resources/trusted_certificates/EID_Q_2024E.der.crt b/src/main/resources/trusted_certificates/EID_Q_2024E.der.crt new file mode 100644 index 0000000000000000000000000000000000000000..2503b7e96dcb250140d075f4080f27e1dbbcb68d GIT binary patch literal 947 zcmXqLVqS01#4NCYnTe5!Nkk=b3F{GkXBWP~62uT`bVFP{U33E#j0}uSTyeVrugj7BXi#P#z{U;^047E@R_#U> z1|{Ys29`;emOJ^L+OW}d*R^|_~ckKH9+O>en)|J#fzGb z-*1TJnZ?97PEDSbe%JD2&LhT04Dz|Bp7=Ge$MkpUcjvdSzH24W2&Z6*FzuPr`pSpM$q z&eqv;n>k$#vkh|DIJDUqSy|Z`8Ceny;ti}}d;_L7{fv^50xNy}WWC~Sz0_3wVDETO zmw0&8#e*Ut9vBIFDXB$z$wegw3UJMgZ6Z+3`N_ovQ2ox11_dBDDzIc4q#LlYu>hG2 z2C-lvgD`^-m~JLUF%OXLVxZmS<>f%#nJLAv1f!Q)s$Y~^oL^d$oLa1(oLW?pnUoy2T}lC8@coc_sP=`XJ-wSu_mP3{(~F-qQ-zoUc(ODV3ao+s)tCj|QAZdP3D6;^w0vmFoXKw6aFlg*xa$;yG zH2fwTR#h4yohkb2;gnyGwjG~cd35b8M$0dYl-B*fy`lM_Lin7`OFr${W^yT3Jj9l3 zfp@?2*` o>^$DnlDRlj(tHIOCa7;jv#p+ib#bww0Gb^QP;4pl0ZtWA zL(ln^^7Zoa!@E&@!8|O6Y6ob53KrP`s$C!!W4jpGQx|_UZ36+HLI5WJBr*Ipi3`Ko z#s^PTV!r=f!~a(Pb&o&_n|wV5NFAXjg=Zj?er;F^LkL;UCj0%;+XZgg4ugfVMA73D zti9Vt$Q$5c)_&PAOMT6mca6P)6XV<&5wneoed0aO!cRL&>e5h?I%Sy$DqiOLi`PCa zXg)k}!JEyzlbyjiFqs`I3bkuOIgUmxOwFiUM{L8Z&s62TPH3t5`PxXKoi}OuhlGg>77R>=-2U*}A3b221>`B;o;j(L&FW6@ zz3po?w}<*>jz%S}!3&e0XF(5t5vov(R@St7Z+rIsunr_$wql_7PIKgtz~pxuvdtv~ zR$J7TJ7jv=^*H*xnNG9Ad(${XC}W-R_4?=A$qzS}(?ShKk|lxbB%a^Y=JU zJ-Bq?7cDY1b&v}o3~uG->ip_wL+)RIIrr9ZG58@CWh&oG@9>m4{=FKZ_! zm)t8$PTzrzoEfgoh@hvO&1*}#ufCUk^|*zr1FQxipi(K60HjbHn5kS!V?79(Mrs0- z@=z|w$89$@Ma}`UQ}y#O)xmSFlphR<>r*HS0txjC?sS0&l%^F5Qdif2AS4H{fx~AT z!0F^ru{e4Onam}L1SB4h%=BP0oY`N5##Tv&t&|KYm?tE0g<^pJ0z>KgpWrC2DEbq> zoDQN@HR6$cunC}0NaYm)Hhgq}HNfvP4MVe6sA!S$ZHYvpr1`<3&+;bmV#z|DC@NOS z<%!5#o>0sW;d41E>DxpLIb1QH%d-)QIbvP}FH%efWL0_`ase;{gkqCoeEPh9clD(a zt}q-Js{mc(Y@|N(^%J|=AlRv+fPMQ0V;{~4Q{)eHRpA+`DI=AC7wT($Dt{@ZJ|To= zkJ~JcJ9@NS@g|?R#xZ!nr*Fsi71l92#;H?b9Z?%scklAJIp`uCnAO2c{Zn%rHX|(g zsis52{XyBqChN-sx8bcvetR}l>C=;}2#bur&}8${{KBe7f}=!tTfY58sB8;HEBjPT z9zFc%e)|(C>-;$iS}oa_^ltwnwbj^^oe#$IhWiYz;P%N2saVOp7lQhrG^-t*xokn% zp8mF1S36&Nq)|M(QgD5xUI`f`PebdA5Q$v`e(Nrc()5h@&ZF+nA~LN6EAn@G^_O)z zo7HVCKec{{5%8*9%i$)0StY+ftQn?GE)}5BC)gT}{(2hp5m(6((@ADdt2Vvtp}Aai%CSjOc;C8Xxd~#ZX8Q6e15q3S6bxJUE5{p$J`TN zIJQ1s=RFoj3|jV>rZAxtA6wl`TIp&|(_dSaTqg66>(0Gw?KHY1sI>RV!gfEc`xZBr zbgg1#R2W$_h{}onvPFoPLI;wDWkTtpR(dw3d*R)4*P)u-tvMuhw`Pm;@zlQo9Lv0j literal 0 HcmV?d00001 diff --git a/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java b/src/test/java/ee/sk/smartid/AuthCodeTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/AuthCodeTest.java rename to src/test/java/ee/sk/smartid/AuthCodeTest.java index d3ead0cc..df990210 100644 --- a/src/test/java/ee/sk/smartid/v3/AuthCodeTest.java +++ b/src/test/java/ee/sk/smartid/AuthCodeTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java similarity index 96% rename from src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java rename to src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java index 200a2993..c18f84dd 100644 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -32,8 +32,6 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.AuthenticationIdentity; - public class AuthenticationIdentityTest { @Test diff --git a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java rename to src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java index 5ea7a00a..2b97fbc7 100644 --- a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -41,14 +41,13 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; -import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; class AuthenticationResponseMapperTest { diff --git a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java similarity index 89% rename from src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java rename to src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 3dafb025..7d5c0028 100644 --- a/src/test/java/ee/sk/smartid/v3/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -42,9 +42,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; @@ -151,6 +148,38 @@ void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidate assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); } + @Test + void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndValidSignature_ok() { + var validator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); + + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForAuthCert")); + dynamicLinkAuthenticationResponse.setServerRandom("serverRandom"); + dynamicLinkAuthenticationResponse.setEndResult("OK"); + + assertThrows(SmartIdClientException.class, () -> validator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + } + + @Test + void toAuthenticationIdentity_certificateHasMatchingKeyButDifferentDN_throwException() { + var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); + dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); + dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForFakeCert")); + dynamicLinkAuthenticationResponse.setServerRandom("serverRandom"); + dynamicLinkAuthenticationResponse.setEndResult("OK"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> + authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + + assertEquals("Signer's certificate is not trusted", exception.getMessage()); + } + @Test void toAuthenticationIdentity_dynamicLinkAuthenticationResponseIsMissing_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(null, null)); diff --git a/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java rename to src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java index a26bd6da..f2c64180 100644 --- a/src/test/java/ee/sk/smartid/v3/CertificateChoiceResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -41,13 +41,12 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; -import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionStatus; public class CertificateChoiceResponseMapperTest { diff --git a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java similarity index 97% rename from src/test/java/ee/sk/smartid/v2/CertificateUtil.java rename to src/test/java/ee/sk/smartid/CertificateUtil.java index 89d5979a..5b084b9b 100644 --- a/src/test/java/ee/sk/smartid/v2/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -31,8 +31,6 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import ee.sk.smartid.CertificateParser; - public final class CertificateUtil { private CertificateUtil() { diff --git a/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java rename to src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java index be3da970..8b8590a2 100644 --- a/src/test/java/ee/sk/smartid/v2/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java rename to src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java index fe2c3fd3..77d126c3 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicContentBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java index b1aa6c7e..51f499ce 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -58,10 +58,10 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; class DynamicLinkAuthenticationSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java index 223546b8..6b603f17 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -41,9 +41,9 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; class DynamicLinkCertificateChoiceSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java index bfd6c768..b312e1f3 100644 --- a/src/test/java/ee/sk/smartid/v3/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -53,14 +53,13 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.mockito.ArgumentCaptor; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; class DynamicLinkSignatureSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java rename to src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java index fc96c859..51c75715 100644 --- a/src/test/java/ee/sk/smartid/v3/ErrorResultHandlerTest.java +++ b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index 0e49b53a..4337c0fc 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -58,11 +58,11 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.VerificationCode; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.VerificationCode; class NotificationAuthenticationSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 34bc7fba..2bd05296 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -55,9 +55,9 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; class NotificationCertificateChoiceSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index cdf9989f..0bf5d788 100644 --- a/src/test/java/ee/sk/smartid/v3/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -54,15 +54,14 @@ import org.junit.jupiter.params.provider.NullSource; import org.mockito.ArgumentCaptor; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v3.rest.dao.VerificationCode; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.VerificationCode; class NotificationSignatureSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java rename to src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index b02a3f93..b45c8ae6 100644 --- a/src/test/java/ee/sk/smartid/v3/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/QrCodeUtil.java b/src/test/java/ee/sk/smartid/QrCodeUtil.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/QrCodeUtil.java rename to src/test/java/ee/sk/smartid/QrCodeUtil.java index 185f1d95..5039bd55 100644 --- a/src/test/java/ee/sk/smartid/v3/QrCodeUtil.java +++ b/src/test/java/ee/sk/smartid/QrCodeUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java b/src/test/java/ee/sk/smartid/RandomChallengeTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java rename to src/test/java/ee/sk/smartid/RandomChallengeTest.java index 1512cb64..06d55431 100644 --- a/src/test/java/ee/sk/smartid/v3/RandomChallengeTest.java +++ b/src/test/java/ee/sk/smartid/RandomChallengeTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java rename to src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java index dd17bcb9..531b5e21 100644 --- a/src/test/java/ee/sk/smartid/v3/SessionEndResultErrorArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java deleted file mode 100644 index f4b22cf4..00000000 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.apache.commons.codec.binary.Base64; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.v2.SignableData; - -public class SignableDataTest { - - private static final byte[] DATA_TO_SIGN = "Hello World!".getBytes(); - private static final String SHA512_HASH_IN_BASE64 = "hhhE1nBOhXP+w02WfiC8/vPUJM9IvgTm3AjyvVjHKXQzcQFerYkcw88cnTS0kmS1EHUbH/nlN5N7xGtdb/TsyA=="; - private static final String SHA384_HASH_IN_BASE64 = "v9dsDrvQBv7lg0EFR8GIewKSvnbVgtlsJC0qeScj4/1v0GH51c/RO4+WE1jmrbpK"; - private static final String SHA256_HASH_IN_BASE64 = "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk="; - - @Test - public void signableData_withDefaultHashType_sha512() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - assertEquals("SHA512", signableData.getHashType().getHashTypeName()); - assertEquals(SHA512_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA512_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("4664", signableData.calculateVerificationCode()); - } - - @Test - public void signableData_with_sha256() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - signableData.setHashType(HashType.SHA256); - assertEquals("SHA256", signableData.getHashType().getHashTypeName()); - assertEquals(SHA256_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA256_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("7712", signableData.calculateVerificationCode()); - } - - @Test - public void signableData_with_sha384() { - SignableData signableData = new SignableData(DATA_TO_SIGN); - signableData.setHashType(HashType.SHA384); - assertEquals("SHA384", signableData.getHashType().getHashTypeName()); - assertEquals(SHA384_HASH_IN_BASE64, signableData.calculateHashInBase64()); - assertArrayEquals(Base64.decodeBase64(SHA384_HASH_IN_BASE64), signableData.calculateHash()); - assertEquals("3486", signableData.calculateVerificationCode()); - } -} diff --git a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java rename to src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java index dce138ce..ee92718a 100644 --- a/src/test/java/ee/sk/smartid/v3/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -36,14 +36,13 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; -import ee.sk.smartid.FileUtil; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.v3.rest.dao.SessionCertificate; -import ee.sk.smartid.v3.rest.dao.SessionResult; -import ee.sk.smartid.v3.rest.dao.SessionSignature; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; class SignatureResponseMapperTest { diff --git a/src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java b/src/test/java/ee/sk/smartid/SignatureUtilTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java rename to src/test/java/ee/sk/smartid/SignatureUtilTest.java index 73c64b7c..3f7e9336 100644 --- a/src/test/java/ee/sk/smartid/v3/SignatureUtilTest.java +++ b/src/test/java/ee/sk/smartid/SignatureUtilTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -35,7 +35,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import ee.sk.smartid.HashType; import ee.sk.smartid.exception.permanent.SmartIdClientException; class SignatureUtilTest { diff --git a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java similarity index 85% rename from src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java rename to src/test/java/ee/sk/smartid/SmartIdClientTest.java index 7e1a957a..2ebc24ee 100644 --- a/src/test/java/ee/sk/smartid/v3/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3; +package ee.sk.smartid; /*- * #%L @@ -44,17 +44,14 @@ import org.junit.jupiter.params.provider.EnumSource; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.HashType; -import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SessionStatus; class SmartIdClientTest { @@ -77,7 +74,7 @@ class DynamicLinkCertificateChoiceSession { @Test void createDynamicLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -99,7 +96,7 @@ class NotificationCertificateChoiceSession { @Test void createNotificationCertificateChoice_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", "requests/certificate-choice-session-request.json", "responses/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -112,7 +109,7 @@ void createNotificationCertificateChoice_withSemanticsIdentifier() { @Test void createNotificationCertificateChoice_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/certificate-choice-session-request.json", "v3/responses/notification-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/document/PNOEE-1234567890-MOCK-Q", "requests/certificate-choice-session-request.json", "responses/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -130,7 +127,7 @@ class DynamicLinkAuthenticationSession { @Test void createDynamicLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -146,7 +143,7 @@ void createDynamicLinkAuthentication_anonymous() { @Test void createDynamicLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") @@ -163,7 +160,7 @@ void createDynamicLinkAuthentication_withDocumentNumber() { @Test void createDynamicLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) @@ -185,7 +182,7 @@ class DynamicLinkSignatureSession { @Test void createDynamicLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/dynamic-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); @@ -206,7 +203,7 @@ void createDynamicLinkSignature_withDocumentNumber() { @Test void createDynamicLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "v3/requests/dynamic-link-signature-request.json", "v3/responses/dynamic-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "requests/dynamic-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); @@ -232,7 +229,7 @@ class NotificationAuthenticationSession { @Test void createNotificationAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) @@ -248,7 +245,7 @@ void createNotificationAuthentication_withSemanticsIdentifier() { @Test void createNotificationAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") @@ -268,7 +265,7 @@ void createNotificationAuthentication_withDocumentNumber() { class NotificationBasedSignatureSession { @Test void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); @@ -289,7 +286,7 @@ void createNotificationSignature_withDocumentNumber() { @Test void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); @@ -317,7 +314,7 @@ class SessionsStatus { @Test void fetchFinalSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-successful-authentication.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-successful-authentication.json"); SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); @@ -327,7 +324,7 @@ void fetchFinalSessionStatus() { @Test void getSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "v3/responses/session-status-running.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-running.json"); SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); @@ -343,7 +340,7 @@ class DynamicContent { @ParameterizedTest @EnumSource void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -368,7 +365,7 @@ void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLin @ParameterizedTest @EnumSource void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -391,7 +388,7 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "v3/requests/certificate-choice-session-request.json", "v3/responses/dynamic-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) diff --git a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java similarity index 96% rename from src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java rename to src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index 0401f22a..bce47f41 100644 --- a/src/test/java/ee/sk/smartid/v2/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2; +package ee.sk.smartid; /*- * #%L @@ -33,9 +33,6 @@ import org.junit.jupiter.api.Test; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - public class VerificationCodeCalculatorTest { diff --git a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java rename to src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java index 695ca767..4f5a2031 100644 --- a/src/test/java/ee/sk/smartid/v2/rest/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.rest.dao; +package ee.sk.smartid.dao; /*- * #%L diff --git a/src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java similarity index 97% rename from src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java rename to src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 8dc7f7e0..2b7cf058 100644 --- a/src/test/java/ee/sk/smartid/v3/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.integration; +package ee.sk.smartid.integration; /*- * #%L @@ -52,29 +52,29 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.AuthCode; -import ee.sk.smartid.v3.AuthenticationCertificateLevel; -import ee.sk.smartid.v3.AuthenticationResponse; -import ee.sk.smartid.v3.AuthenticationResponseMapper; -import ee.sk.smartid.v3.AuthenticationResponseValidator; -import ee.sk.smartid.v3.CertificateChoiceResponse; -import ee.sk.smartid.v3.CertificateChoiceResponseMapper; -import ee.sk.smartid.v3.CertificateLevel; -import ee.sk.smartid.v3.DynamicLinkType; -import ee.sk.smartid.v3.RandomChallenge; -import ee.sk.smartid.v3.SessionType; -import ee.sk.smartid.v3.SignableData; -import ee.sk.smartid.v3.SignatureResponse; -import ee.sk.smartid.v3.SignatureResponseMapper; -import ee.sk.smartid.v3.SmartIdClient; -import ee.sk.smartid.v3.rest.SessionStatusPoller; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.AuthCode; +import ee.sk.smartid.AuthenticationCertificateLevel; +import ee.sk.smartid.AuthenticationResponse; +import ee.sk.smartid.AuthenticationResponseMapper; +import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.CertificateChoiceResponse; +import ee.sk.smartid.CertificateChoiceResponseMapper; +import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.DynamicLinkType; +import ee.sk.smartid.RandomChallenge; +import ee.sk.smartid.SessionType; +import ee.sk.smartid.SignableData; +import ee.sk.smartid.SignatureResponse; +import ee.sk.smartid.SignatureResponseMapper; +import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SessionStatus; @Disabled("Replace relying party UUID and name with your own values in setup") diff --git a/src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java similarity index 94% rename from src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java rename to src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 8b7f3a24..d3abbec7 100644 --- a/src/test/java/ee/sk/smartid/v3/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.integration; +package ee.sk.smartid.integration; /*- * #%L @@ -43,22 +43,22 @@ import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.RandomChallenge; -import ee.sk.smartid.v3.SignatureAlgorithm; -import ee.sk.smartid.v3.rest.SmartIdConnector; -import ee.sk.smartid.v3.rest.SmartIdRestConnector; -import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.RequestProperties; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.RandomChallenge; +import ee.sk.smartid.SignatureAlgorithm; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; +import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; @Disabled("Relying party demo account not yet available for v3") @SmartIdDemoIntegrationTest diff --git a/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java similarity index 94% rename from src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java rename to src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java index a61b3af9..b50ab0a9 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest; +package ee.sk.smartid.rest; /*- * #%L @@ -36,7 +36,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import ee.sk.smartid.v3.rest.dao.SessionStatus; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.SessionStatus; class SessionStatusPollerTest { diff --git a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java similarity index 90% rename from src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java rename to src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index a40a55f1..984b5927 100644 --- a/src/test/java/ee/sk/smartid/v3/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v3.rest; +package ee.sk.smartid.rest; /*- * #%L @@ -60,19 +60,19 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DynamicLinkInteraction; +import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationInteraction; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v3.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v3.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.v3.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.v3.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.v3.rest.dao.NotificationInteraction; -import ee.sk.smartid.v3.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.v3.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.v3.rest.dao.SessionStatus; -import ee.sk.smartid.v3.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; class SmartIdRestConnectorTest { @@ -93,14 +93,14 @@ void setUp() { @Test void getSessionStatus_running() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-running.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); } @Test void getSessionStatus_running_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-running-with-ignored-properties.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running-with-ignored-properties.json"); assertNotNull(sessionStatus); assertEquals("RUNNING", sessionStatus.getState()); assertNotNull(sessionStatus.getIgnoredProperties()); @@ -111,7 +111,7 @@ void getSessionStatus_running_withIgnoredProperties() { @Test void getSessionStatus_forSuccessfulAuthenticationRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-authentication.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); assertSuccessfulResponse(sessionStatus); assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); @@ -128,7 +128,7 @@ void getSessionStatus_forSuccessfulAuthenticationRequest() { @Test void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-certificate-choice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-certificate-choice.json"); assertSuccessfulResponse(sessionStatus); assertNotNull(sessionStatus.getCert()); @@ -138,7 +138,7 @@ void getSessionStatus_forSuccessfulCertificateRequest() { @Test void getSessionStatus_forSuccessfulSignatureRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-signature.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); assertSuccessfulResponse(sessionStatus); assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); @@ -154,7 +154,7 @@ void getSessionStatus_forSuccessfulSignatureRequest() { @Test void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-successful-authentication.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); assertSuccessfulResponse(sessionStatus); verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) @@ -164,7 +164,7 @@ void getSessionStatus_hasUserAgentHeader() { @Test void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v3/responses/session-status-successful-authentication.json"); + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/session-status-successful-authentication.json"); connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); assertSuccessfulResponse(sessionStatus); @@ -181,55 +181,55 @@ void getSessionStatus_whenSessionNotFound() { @Test void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); } @Test void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-confirmation.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); } @Test void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-confirmation-vc-choice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); } @Test void getSessionStatus_userHasRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-display-text-and-pin.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); } @Test void getSessionStatus_userHasRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-vc-choice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); } @Test void getSessionStatus_userHasRefusedCertChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-user-refused-cert-choice.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CERT_CHOICE"); } @Test void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-timeout.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); } @Test void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-wrong-vc.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); } @Test void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v3/responses/session-status-document-unusable.json"); + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); } @@ -266,7 +266,7 @@ void setUp() { @Test void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); @@ -278,7 +278,7 @@ void initDynamicLinkAuthentication() { @Test void initDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -286,7 +286,7 @@ void initDynamicLinkAuthentication_userAccountNotFound_throwException() { @Test void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -308,8 +308,8 @@ void setUp() { @Test void initDynamicLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "v3/requests/dynamic-link-authentication-session-request.json", - "v3/responses/dynamic-link-authentication-session-response.json"); + "requests/dynamic-link-authentication-session-request.json", + "responses/dynamic-link-authentication-session-response.json"); Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); @@ -322,7 +322,7 @@ void initDynamicLinkAuthentication() { @Test void initDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -330,7 +330,7 @@ void initDynamicLinkAuthentication_userAccountNotFound_throwException() { @Test void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -351,7 +351,7 @@ void setUp() { @Test void initAnonymousDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json", "v3/responses/dynamic-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); Instant start = Instant.now(); DynamicLinkSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); @@ -363,7 +363,7 @@ void initAnonymousDynamicLinkAuthentication() { @Test void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); }); } @@ -371,7 +371,7 @@ void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() @Test void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "v3/requests/dynamic-link-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json"); connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); }); } @@ -392,7 +392,7 @@ void setUp() { @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); assertNotNull(response); @@ -401,7 +401,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -409,7 +409,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -430,7 +430,7 @@ void setUp() { @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); assertNotNull(response); @@ -439,7 +439,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -447,7 +447,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "v3/requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -468,7 +468,7 @@ public void setUp() { @Test void initDynamicLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "v3/responses/dynamic-link-certificate-choice-session-response.json"); + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/dynamic-link-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); @@ -581,7 +581,7 @@ public void setUp() { @Test void initCertificateChoice_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/responses/notification-certificate-choice-session-response.json"); + stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "responses/notification-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -595,7 +595,7 @@ void initCertificateChoice_withSemanticsIdentifier_successful() { @Test void initCertificateChoice_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } @@ -603,7 +603,7 @@ void initCertificateChoice_userAccountNotFound_throwException() { @Test void initCertificateChoice_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } @@ -625,7 +625,7 @@ public void setUp() { @Test void initCertificateChoice_withDocumentNumber_successful() { - stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/responses/notification-certificate-choice-session-response.json"); + stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "responses/notification-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); String documentNumber = "PNOEE-48010010101-MOCK-Q"; @@ -639,7 +639,7 @@ void initCertificateChoice_withDocumentNumber_successful() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -647,7 +647,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "v3/requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "requests/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -669,7 +669,7 @@ public void setUp() { @Test void initDynamicLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/responses/dynamic-link-signature-session-response.json"); + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/dynamic-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -684,7 +684,7 @@ void initDynamicLinkSignature_withSemanticsIdentifier_successful() { @Test void initDynamicLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111-MOCK-Q", "v3/responses/dynamic-link-signature-session-response.json"); + stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111-MOCK-Q", "responses/dynamic-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111-MOCK-Q"; @@ -799,7 +799,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -815,7 +815,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -827,7 +827,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -904,7 +904,7 @@ void setUp() { @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json", "v3/responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -920,7 +920,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -932,7 +932,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "v3/requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); diff --git a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java similarity index 98% rename from src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java rename to src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index 60371d39..0efebf0d 100644 --- a/src/test/java/ee/sk/smartid/v2/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.util; +package ee.sk.smartid.util; /*- * #%L @@ -48,8 +48,7 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.v2.CertificateUtil; +import ee.sk.smartid.CertificateUtil; public class CertificateAttributeUtilTest { diff --git a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java similarity index 96% rename from src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java rename to src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java index ed3cf0c9..a3e9fdad 100644 --- a/src/test/java/ee/sk/smartid/v2/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.v2.util; +package ee.sk.smartid.util; /*- * #%L @@ -41,10 +41,9 @@ import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentityMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.NationalIdentityNumberUtil; -import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.v2.CertificateUtil; +import ee.sk.smartid.CertificateUtil; public class NationalIdentityNumberUtilTest { @@ -56,7 +55,7 @@ public class NationalIdentityNumberUtilTest { public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { X509Certificate eeCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE); - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(eeCertificate); + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); @@ -68,7 +67,7 @@ public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws Certificate public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { X509Certificate lvCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(lvCertificate); + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); @@ -80,7 +79,7 @@ public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateE public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { X509Certificate ltCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LT); - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(ltCertificate); + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java deleted file mode 100644 index 1f52f305..00000000 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationRequestBuilderTest.java +++ /dev/null @@ -1,628 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.v2.DummyData.createSessionEndResult; -import static ee.sk.smartid.v2.DummyData.createUserRefusedSessionStatus; -import static ee.sk.smartid.v2.DummyData.createUserSelectedWrongVerificationCode; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.CertificateEncodingException; -import java.util.Collections; - -import org.apache.commons.codec.binary.Base64; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SessionCertificate; -import ee.sk.smartid.v2.rest.dao.SessionSignature; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -public class AuthenticationRequestBuilderTest { - - private SmartIdConnectorSpy connector; - private AuthenticationRequestBuilder builder; - - @BeforeEach - public void setUp() { - connector = new SmartIdConnectorSpy(); - connector.authenticationSessionResponseToRespond = createDummyAuthenticationSessionResponse(); - connector.sessionStatusToRespond = createDummySessionStatusResponse(); - builder = new AuthenticationRequestBuilder(connector, new SessionStatusPoller(connector)); - } - - @Test - public void authenticateWithDocumentNumberAndGeneratedHash() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticateWithHash() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities("ADVANCED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==", "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticate_usingSemanticsIdentifier() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withSemanticsIdentifier(new SemanticsIdentifier("IDCCZ-1234567890")) - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticate_usingSemanticsIdentifierAsString() throws Exception { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - } - - @Test - public void authenticateWithoutCertificateLevel_shouldPass() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), null); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withShareMdClientIpAddressTrue() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(true) - .authenticate(); - - Assertions.assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - Assertions.assertTrue(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withShareMdClientIpAddressFalse() throws Exception { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - SmartIdAuthenticationResponse authenticationResponse = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(false) - .authenticate(); - - assertCorrectAuthenticationRequestMadeWithDocumentNumber(authenticationHash.getHashInBase64(), "QUALIFIED"); - - Assertions.assertNotNull(connector.authenticationSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - - Assertions.assertFalse(connector.authenticationSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); - - assertCorrectSessionRequestMade(); - assertAuthenticationResponseCorrect(authenticationResponse, authenticationHash.getHashInBase64()); - } - - @Test - public void authenticate_withoutDocumentNumber_withoutSemanticsIdentifier_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Exactly one of documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_withoutHashAndWithoutDataToSign_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, - () -> builder.withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate()); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticateWithHash_withoutHashType_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticateWithHash_withoutHash_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withAuthenticationHash(authenticationHash) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticateWithoutRelyingPartyUuid_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Parameter relyingPartyUUID must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticateWithoutRelyingPartyName_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .authenticate(); - }); - assertEquals("Parameter relyingPartyName must be set", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_withTooLongNonce_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .authenticate(); - }); - assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_missingAllowedInteractionOrder_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .authenticate(); - }); - assertEquals("Missing or empty mandatory parameter allowedInteractionsOrder", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) - ) - .authenticate(); - }); - assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) - ) - .authenticate(); - }); - assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .authenticate(); - }); - assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .authenticate(); - }); - assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_userRefused_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userRefusedCertChoice_shouldThrowException() { - assertThrows(UserRefusedCertChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userRefusedDisplayTextAndPin_shouldThrowException() { - assertThrows(UserRefusedDisplayTextAndPinException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userRefusedVerificationChoice_shouldThrowException() { - assertThrows(UserRefusedVerificationChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userRefusedConfirmationMessage_shouldThrowException() { - assertThrows(UserRefusedConfirmationMessageException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { - assertThrows(UserRefusedConfirmationMessageWithVerificationChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_userSelectedWrongVerificationCode_shouldThrowException() { - assertThrows(UserSelectedWrongVerificationCodeException.class, () -> { - connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_resultMissingInResponse_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.setResult(null); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_signatureMissingInResponse_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.setSignature(null); - makeAuthenticationRequest(); - }); - } - - @Test - public void authenticate_certificateMissingInResponse_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.setCert(null); - makeAuthenticationRequest(); - }); - } - - private void assertCorrectAuthenticationRequestMadeWithDocumentNumber(String expectedHashToSignInBase64, String expectedCertificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - Assertions.assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - Assertions.assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - Assertions.assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - Assertions.assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - Assertions.assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); - } - - private void assertCorrectAuthenticationRequestMadeWithSemanticsIdentifier(String expectedHashToSignInBase64, String expectedCertificateLevel) { - Assertions.assertEquals("IDCCZ-1234567890", connector.semanticsIdentifierUsed.getIdentifier()); - Assertions.assertEquals("relying-party-uuid", connector.authenticationSessionRequestUsed.getRelyingPartyUUID()); - Assertions.assertEquals("relying-party-name", connector.authenticationSessionRequestUsed.getRelyingPartyName()); - Assertions.assertEquals(expectedCertificateLevel, connector.authenticationSessionRequestUsed.getCertificateLevel()); - Assertions.assertEquals("SHA512", connector.authenticationSessionRequestUsed.getHashType()); - Assertions.assertEquals(expectedHashToSignInBase64, connector.authenticationSessionRequestUsed.getHash()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertAuthenticationResponseCorrect(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) throws CertificateEncodingException { - assertNotNull(authenticationResponse); - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); - assertEquals("c2FtcGxlIHNpZ25hdHVyZQ0K", authenticationResponse.getSignatureValueInBase64()); - assertEquals("sha512WithRSAEncryption", authenticationResponse.getAlgorithmName()); - assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(authenticationResponse.getCertificate().getEncoded())); - assertEquals("QUALIFIED", authenticationResponse.getCertificateLevel()); - - assertThat(authenticationResponse.getInteractionFlowUsed(), is("displayTextAndPIN")); - } - - private AuthenticationSessionResponse createDummyAuthenticationSessionResponse() { - AuthenticationSessionResponse response = new AuthenticationSessionResponse(); - response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return response; - } - - private SessionStatus createDummySessionStatusResponse() { - SessionSignature signature = new SessionSignature(); - signature.setValue("c2FtcGxlIHNpZ25hdHVyZQ0K"); - signature.setAlgorithm("sha512WithRSAEncryption"); - - SessionCertificate certificate = new SessionCertificate(); - certificate.setCertificateLevel("QUALIFIED"); - certificate.setValue(DummyData.CERTIFICATE); - - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setResult(createSessionEndResult()); - status.setSignature(signature); - status.setCert(certificate); - status.setInteractionFlowUsed("displayTextAndPIN"); - status.setDeviceIpAddress("4.4.4.4"); - return status; - } - - private void makeAuthenticationRequest() { - AuthenticationHash authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - authenticationHash.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?"))) - .authenticate(); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java deleted file mode 100644 index 4ec139c2..00000000 --- a/src/test/java/ee/sk/smartid/v2/AuthenticationResponseValidatorTest.java +++ /dev/null @@ -1,359 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; - -import javax.security.auth.x500.X500Principal; - -import org.apache.commons.codec.binary.Base64; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; - -public class AuthenticationResponseValidatorTest { - - private static final String VALID_SIGNATURE_IN_BASE64 = "F84UserdWKmmsZeu5trpMT+yhqZ3aMYMhQatSrRkq3TrYWS/xaE1yzmuzNdYXELs3ZGURuXsePfPKFBvc+PTU7oRHT8dxq3zuAqhDZO8VN5iWKpjF0LTwcA4sO6+uw5hXewG/e8I/CutyYlfcobFvLIqXvXXLl2fcAeQbMvKhj/6yuwwz3b7INVDKQnz/8y+v5/XXBFnlniNJNx7d4Kk+IL7r3DMzttKrldOUzUOuIVb6sdBcrg0+LWClMIt6nCP+T006iRruGqvPpbIsEOs2JIuZo3eh7j6nX2xtMzzgd87BDUzHIFJTj8ZVQu/Yp5A4O3iL2k3E+oOX/5wQkleC6sJ94M6kPliK0LCBv7xcMUmSnwPR3ZjNCX315F21k+ikwK6JlXxBS9pvfLNi2574112yBCq4hB7VKRdORSja9XF4jhoL/rbqisuHRqIMCg3weK6dprSJB1+3pyDGzYPLsV+6RnAb958e/0A7Mq1wg4qjjlqpn32CifoGbwABjUzBhOJC/IFp5ftVQfq3KPLPviyHZN8uIuwwDfI3A9PIOOqu5jt31G777DKGW1xMwd3yRErZ2fbNbNAKjpjeNQtQmS0rcX+l0efBMe4PCmRpT3Sv0i/vNkTlZfqB2NkVSLzTevDt0N1UU+N6u4v5ZEmuEqtoXGWT4ZRlUTUc1oUG8w="; - - private static final String INVALID_SIGNATURE_IN_BASE64 = "XDzm10vKbvMMKv+o7i/Sz726hbcKPiWxtmP8Wc68v5BnJOp+STDhyq18CEAyIG/ucmlRi/TtTFn+7r6jNEczZ+2wIlDq7J8WJ3TKbAiCUUAoFccon2fqXAZHGceO/pRfrEbVsy6Oh9HodOwr/7A1a46JCCif9w/1ZE84Tm1RVsJHSkBdKYFOPTCEbN2AXZXDU9qshIyjLHrIyZ3ve6ay6L2xCyK1VOY6y3zsavzxd2CjAkvk9l1MrMLKOoI4lHXmIqDTr1I5ixMZ/g05aua0AHGE/cOp1XRj5lRJW48kjISidH9lPdnEHTKZJ6SFc/ZpZOYt7W+BNMb2dcvgOWrRXICPy0KfAh6gRAJIOUe6kPhIqvGnZ450fX1eO5wd957a1Tjlw6+h7AGf1YFYciLBpC+D3k/E8VDJUoicJBfzGFjEhd4xJYFGw3ZqUWr7dF/6LLSBpL1B87kHhsFhpn+3h0AWJaSqkD1DW3upSdlTZOV+IqoPlTMzV6HJn1yOGrg+yWBiCX1Xs7NbbMveyg/7E/wxVYOaaXGeXp4yaLxS1YJMu0PiQByvhZyarEPWEc6imlmg6LKUYzu6rklcQL7dW8xUW7n6gLx+Jyh+4KVyom968LtjC8zXCkL+VkiWRQIbOx6+k/q+4/aR9tG9rgjMCSV5kYn+kLRGfNA8eHp891c="; - - private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; - private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; - private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; - - private static final String HASH_TO_SIGN_IN_BASE64 = "pcWJTcOvmk5Xcvyfrit9SF55S3qU+NfEEVxg4fVf+GdxMN0W2wSpJVivcf91IG+Ji3aCGlNN8p5scBEn6mgUOg=="; - - private AuthenticationResponseValidator validator; - - @BeforeEach - public void setUp() { - validator = new AuthenticationResponseValidator(); - } - - @Test - public void validate() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void validate_invalidSignatureValue() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setSignatureValueInBase64("invalid"); - - validator.validate(response); - }); - - assertEquals("Failed to verify validity of signature returned by Smart-ID", unprocessableSmartIdResponseException.getMessage()); - } - - @Test - public void validationReturnsValidAuthenticationResult_whenEndResultLowerCase_shouldPass() { - - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setEndResult("ok"); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenEndResultNotOk() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidEndResult(); - validator.validate(response); - }); - assertEquals("Smart-ID API returned end result code 'NOT OK'", unprocessableSmartIdResponseException.getMessage()); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignatureVerificationFails() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidationResponseWithInvalidSignature(); - validator.validate(response); - }); - assertEquals("Failed to verify validity of signature returned by Smart-ID", unprocessableSmartIdResponseException.getMessage()); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignersCertExpired() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidationResponseWithExpiredCertificate(); - validator.validate(response); - }); - assertEquals("Signer's certificate has expired", unprocessableSmartIdResponseException.getMessage()); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenSignersCertNotTrusted() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(Base64.decodeBase64(AUTH_CERTIFICATE_EE)); - - validator.validate(response); - }); - assertEquals("Signer's certificate is not trusted", unprocessableSmartIdResponseException.getMessage()); - } - - @Test - public void validationReturnsValidAuthenticationResult_whenCertificateLevelHigherThanRequested_shouldPass() { - SmartIdAuthenticationResponse response = createValidationResponseWithHigherCertificateLevelThanRequested(); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void validationReturnsInvalidAuthenticationResult_whenCertificateLevelLowerThanRequested() { - var certificateLevelMismatchException = assertThrows(CertificateLevelMismatchException.class, () -> { - SmartIdAuthenticationResponse response = createValidationResponseWithLowerCertificateLevelThanRequested(); - validator.validate(response); - }); - assertEquals("Signer's certificate is below requested certificate level", certificateLevelMismatchException.getMessage()); - } - - @Test - public void testTrustedCACertificateLoadingInPEMFormat() throws CertificateException { - byte[] caCertificateInPem = CertificateUtil.getX509CertificateBytes(AUTH_CERTIFICATE_EE); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateInPem); - - X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE).getSubjectX500Principal(); - assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); - } - - @Test - public void testTrustedCACertificateLoadingInDERFormat() throws CertificateException { - byte[] caCertificateInDER = Base64.decodeBase64(AUTH_CERTIFICATE_EE); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateInDER); - - X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE).getSubjectX500Principal(); - assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); - } - - @Test - public void testTrustedCACertificateLoadingFromFile() throws IOException, CertificateException { - File caCertificateFile = new File(AuthenticationResponseValidatorTest.class.getResource("/trusted_certificates/TEST_of_EID-SK_2016.pem.crt").getFile()); - - AuthenticationResponseValidator validator = new AuthenticationResponseValidator(); - validator.clearTrustedCACertificates(); - validator.addTrustedCACertificate(caCertificateFile); - - X500Principal expectedPrincipal = CertificateUtil.getX509Certificate(Files.readAllBytes(caCertificateFile.toPath())).getSubjectX500Principal(); - assertEquals(expectedPrincipal, validator.getTrustedCACertificates().get(0).getSubjectX500Principal()); - } - - @Test - public void withEmptyRequestedCertificateLevel_shouldPass() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setRequestedCertificateLevel(""); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void withNullRequestedCertificateLevel_shouldPass() { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setRequestedCertificateLevel(null); - AuthenticationIdentity authenticationIdentity = validator.validate(response); - - assertAuthenticationIdentityValid(authenticationIdentity, response.getCertificate()); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void whenCertificateIsNull_ThenThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setCertificate(null); - validator.validate(response); - }); - } - - @Test - public void whenSignatureIsEmpty_ThenThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setSignatureValueInBase64(""); - validator.validate(response); - }); - } - - @Test - public void whenHashTypeIsNull_ThenThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse response = createValidValidationResponse(); - response.setHashType(null); - validator.validate(response); - }); - } - - @Test - public void shouldConstructAuthenticationIdentityEE() throws CertificateException { - X509Certificate certificateEe = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateEe); - - assertThat(authenticationIdentity.getIdentityNumber(), is("10101010005")); - assertThat(authenticationIdentity.getCountry(), is("EE")); - assertThat(authenticationIdentity.getGivenName(), is("DEMO")); - assertThat(authenticationIdentity.getSurname(), is("SMART-ID")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void shouldConstructAuthenticationIdentityLV() throws CertificateException { - X509Certificate certificateLv = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLv); - - assertThat(authenticationIdentity.getIdentityNumber(), is("010117-21234")); - assertThat(authenticationIdentity.getCountry(), is("LV")); - assertThat(authenticationIdentity.getGivenName(), is("FORENAME-010117-21234")); - assertThat(authenticationIdentity.getSurname(), is("SURNAME-010117-21234")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(2017, 1, 1))); - } - - @Test - public void shouldConstructAuthenticationIdentityLT() throws CertificateException { - X509Certificate certificateLt = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LT); - - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateLt); - - assertThat(authenticationIdentity.getIdentityNumber(), is("36009067968")); - assertThat(authenticationIdentity.getCountry(), is("LT")); - assertThat(authenticationIdentity.getGivenName(), is("FORENAMEPNOLT-36009067968")); - assertThat(authenticationIdentity.getSurname(), is("SURNAMEPNOLT-36009067968")); - - assertThat(authenticationIdentity.getDateOfBirth().isPresent(), is(true)); - assertThat(authenticationIdentity.getDateOfBirth().get(), is(LocalDate.of(1960, 9, 6))); - } - - private SmartIdAuthenticationResponse createValidValidationResponse() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithInvalidEndResult() { - return createValidationResponse("NOT OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithInvalidSignature() { - return createValidationResponse("OK", INVALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithLowerCertificateLevelThanRequested() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "ADVANCED", "QUALIFIED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithHigherCertificateLevelThanRequested() { - return createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "ADVANCED"); - } - - private SmartIdAuthenticationResponse createValidationResponseWithExpiredCertificate() { - SmartIdAuthenticationResponse response = createValidationResponse("OK", VALID_SIGNATURE_IN_BASE64, "QUALIFIED", "QUALIFIED"); - - String expiredCertificate = "MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkGA1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYDVQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0GCSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51IShSj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+MllSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5czGP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdcDmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncvpRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhNBYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSnoJPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoAxoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdFTOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDiG7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbrrl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxMTEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y52n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAKGn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbRR1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2PMnXRUgtlnV7ykFpmkR4JcQ=="; - X509Certificate expiredX509Certificate = CertificateParser.parseX509Certificate(expiredCertificate); - response.setCertificate(expiredX509Certificate); - return response; - } - - private SmartIdAuthenticationResponse createValidationResponse(String endResult, String signatureInBase64, String certificateLevel, String requestedCertificateLevel) { - SmartIdAuthenticationResponse authenticationResponse = new SmartIdAuthenticationResponse(); - authenticationResponse.setEndResult(endResult); - authenticationResponse.setSignatureValueInBase64(signatureInBase64); - authenticationResponse.setCertificate(CertificateParser.parseX509Certificate(AUTH_CERTIFICATE_EE)); - authenticationResponse.setSignedHashInBase64(HASH_TO_SIGN_IN_BASE64); - authenticationResponse.setHashType(HashType.SHA512); - authenticationResponse.setRequestedCertificateLevel(requestedCertificateLevel); - authenticationResponse.setCertificateLevel(certificateLevel); - return authenticationResponse; - } - - private void assertAuthenticationIdentityValid(AuthenticationIdentity authenticationIdentity, X509Certificate certificate) { - String distinguishedName = certificate.getSubjectX500Principal().getName(); - var x500name = new X500Name(distinguishedName); - assertEquals(getAttribute(x500name, BCStyle.GIVENNAME), authenticationIdentity.getGivenName()); - assertEquals(getAttribute(x500name, BCStyle.SURNAME), authenticationIdentity.getSurname()); - assertEquals(getAttribute(x500name, BCStyle.SERIALNUMBER).split("-", 2)[1], authenticationIdentity.getIdentityNumber()); - assertEquals(getAttribute(x500name, BCStyle.C), authenticationIdentity.getCountry()); - } - - private static String getAttribute(X500Name x500name, ASN1ObjectIdentifier oid) { - RDN[] rdns = x500name.getRDNs(oid); - return IETFUtils.valueToString(rdns[0].getFirst().getValue()); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java b/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java deleted file mode 100644 index 42d5c1f5..00000000 --- a/src/test/java/ee/sk/smartid/v2/CertificateLevelTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class CertificateLevelTest { - - @Test - public void testBothCertificateLevelsQualified() { - String certificateLevelString = "QUALIFIED"; - CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); - assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); - } - - @Test - public void testBothCertificateLevelsAdvanced() { - String certificateLevelString = "ADVANCED"; - CertificateLevel certificateLevel = new CertificateLevel(certificateLevelString); - assertTrue(certificateLevel.isEqualOrAbove(certificateLevelString)); - } - - @Test - public void testFirstCertificateLevelHigher() { - CertificateLevel certificateLevel = new CertificateLevel("QUALIFIED"); - assertTrue(certificateLevel.isEqualOrAbove("ADVANCED")); - } - - @Test - public void testFirstCertificateLevelLower() { - CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); - assertFalse(certificateLevel.isEqualOrAbove("QUALIFIED")); - } - - @Test - public void testFirstCertLevelUnknown() { - CertificateLevel certificateLevel = new CertificateLevel("SOME UNKNOWN LEVEL"); - assertFalse(certificateLevel.isEqualOrAbove("ADVANCED")); - } - - @Test - public void testSecondCertLevelUnknown() { - CertificateLevel certificateLevel = new CertificateLevel("ADVANCED"); - assertFalse(certificateLevel.isEqualOrAbove("SOME UNKNOWN LEVEL")); - } - - @Test - public void certificateLevel_nullArgumentToConstructor() { - assertThrows(IllegalArgumentException.class, () -> new CertificateLevel(null)); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java deleted file mode 100644 index f58f2ef4..00000000 --- a/src/test/java/ee/sk/smartid/v2/CertificateRequestBuilderTest.java +++ /dev/null @@ -1,341 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.X509Certificate; - -import javax.security.auth.x500.X500Principal; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.SessionCertificate; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -public class CertificateRequestBuilderTest { - - private SmartIdConnectorSpy connector; - private CertificateRequestBuilder builder; - - @BeforeEach - public void setUp() { - connector = new SmartIdConnectorSpy(); - SessionStatusPoller sessionStatusPoller = new SessionStatusPoller(connector); - connector.sessionStatusToRespond = createCertificateSessionStatusCompleteResponse(); - connector.certificateChoiceToRespond = createCertificateChoiceResponse(); - builder = new CertificateRequestBuilder(connector, sessionStatusPoller); - } - - @Test - public void getCertificate_usingSemanticsIdentifier() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifierAsString("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("QUALIFIED"); - } - - @Test - public void getCertificate_usingDocumentNumber() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withCapabilities("ADVANCED") - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateRequestMadeWithDocumentNumber("QUALIFIED"); - } - - @Test - public void getCertificate_withoutAnyIdentifier_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .fetch(); - }); - assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); - } - - @Test - public void getCertificate_withoutCertificateLevel() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .fetch(); - assertCertificateResponseValid(certificate); - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade(null); - } - - @Test - public void getCertificate_withShareMdClientIpAddressTrue() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withShareMdClientIpAddress(true) - .fetch(); - assertCertificateResponseValid(certificate); - - Assertions.assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - Assertions.assertTrue(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be true"); - assertThat(certificate.getDeviceIpAddress(), is("5.5.5.5")); - - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("ADVANCED"); - } - - @Test - public void getCertificate_withShareMdClientIpAddressFalse() { - SmartIdCertificate certificate = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withShareMdClientIpAddress(false) - .fetch(); - assertCertificateResponseValid(certificate); - - Assertions.assertNotNull(connector.certificateRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - Assertions.assertFalse(connector.certificateRequestUsed.getRequestProperties().getShareMdClientIpAddress(), "requestProperties.shareMdClientIpAddress must be false"); - - assertCorrectSessionRequestMade(); - assertValidCertificateChoiceRequestMade("ADVANCED"); - } - - @Test - public void getCertificate_whenIdentityOrDocumentNumberNotSet_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .fetch() - ); - } - - @Test - public void getCertificate_withoutRelyingPartyUUID_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> - builder - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch() - ); - } - - @Test - public void getCertificate_withoutRelyingPartyName_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> - builder - .withRelyingPartyUUID("relying-party-uuid") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch() - ); - } - - @Test - public void getCertificate_withTooLongNonce_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, - () -> builder.withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .fetch()); - assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); - } - - @Test - public void getCertificate_withCapabilities() { - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("QUALIFIED") - .withCapabilities(Capability.ADVANCED) - .fetch(); - } - - @Test - public void getCertificate_whenUserRefuses_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = DummyData.createUserRefusedSessionStatus("USER_REFUSED"); - makeCertificateRequest(); - }); - } - - @Test - public void getCertificate_withDocumentNumber_whenUserRefuses_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = DummyData.createUserRefusedSessionStatus("USER_REFUSED"); - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .fetch(); - }); - } - - @Test - public void getCertificate_withCertificateResponseWithoutCertificate_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.setCert(null); - makeCertificateRequest(); - }); - } - - @Test - public void getCertificate_withCertificateResponseContainingEmptyCertificate_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.getCert().setValue(""); - makeCertificateRequest(); - }); - } - - @Test - public void getCertificate_withCertificateResponseWithoutDocumentNumber_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.getResult().setDocumentNumber(null); - makeCertificateRequest(); - }); - } - - @Test - public void getCertificate_withCertificateResponseWithBlankDocumentNumber_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.getResult().setDocumentNumber(""); - makeCertificateRequest(); - }); - } - - private void assertCertificateResponseValid(SmartIdCertificate certificate) { - assertNotNull(certificate); - assertNotNull(certificate.getCertificate()); - X509Certificate cert = certificate.getCertificate(); - String serialNumber = getAttributeValue(cert.getSubjectX500Principal(), BCStyle.SERIALNUMBER); - assertEquals("PNOEE-31111111111", serialNumber); - assertEquals("QUALIFIED", certificate.getCertificateLevel()); - assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertValidCertificateChoiceRequestMade(String certificateLevel) { - MatcherAssert.assertThat(connector.semanticsIdentifierUsed.getIdentifier(), is("PNOEE-31111111111")); - - Assertions.assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - Assertions.assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - Assertions.assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); - } - - private void assertValidCertificateRequestMadeWithDocumentNumber(String certificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - Assertions.assertEquals("relying-party-uuid", connector.certificateRequestUsed.getRelyingPartyUUID()); - Assertions.assertEquals("relying-party-name", connector.certificateRequestUsed.getRelyingPartyName()); - Assertions.assertEquals(certificateLevel, connector.certificateRequestUsed.getCertificateLevel()); - } - - private SessionStatus createCertificateSessionStatusCompleteResponse() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setCert(createSessionCertificate()); - status.setResult(DummyData.createSessionEndResult()); - status.setDeviceIpAddress("5.5.5.5"); - return status; - } - - private SessionCertificate createSessionCertificate() { - SessionCertificate sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(DummyData.CERTIFICATE); - return sessionCertificate; - } - - private CertificateChoiceResponse createCertificateChoiceResponse() { - CertificateChoiceResponse certificateChoiceResponse = new CertificateChoiceResponse(); - certificateChoiceResponse.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return certificateChoiceResponse; - } - - private void makeCertificateRequest() { - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("QUALIFIED") - .fetch(); - } - - private static String getAttributeValue(X500Principal subjectX500Principal, ASN1ObjectIdentifier oid) { - var x500name = new X500Name(subjectX500Principal.getName()); - RDN[] rdns = x500name.getRDNs(oid); - return IETFUtils.valueToString(rdns[0].getFirst().getValue()); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/DummyData.java b/src/test/java/ee/sk/smartid/v2/DummyData.java deleted file mode 100644 index d94fdb29..00000000 --- a/src/test/java/ee/sk/smartid/v2/DummyData.java +++ /dev/null @@ -1,65 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.v2.rest.dao.SessionResult; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -public class DummyData { - - public static final String CERTIFICATE = "MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMTYxMjA5MTYyNDU2WhcNMTkxMjA5MTYyNDU2WjCBvzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMS0wKwYDVQQDDCRFTEZSSUlEQSxNQU5JVkFMREUsUE5PRUUtMzExMTExMTExMTExETAPBgNVBAQMCEVMRlJJSURBMRIwEAYDVQQqDAlNQU5JVkFMREUxGjAYBgNVBAUTEVBOT0VFLTMxMTExMTExMTExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgcfk+eY6dvVyDDPpJPkoKpQ08pQx5Jpfjgq+G31lRSsx03y4WYWQhILu5R4isI6DGzQ1MK2dEsW9Dl+S39y7mDDqGlviVpxCtgz14H7NG84ew8vd+sBeaYCvEhKS4+FxRWCmg5VCozr3s2Evi/ao3Wj51ThtecVmAY7PoE27Zckr0GJ/0I+JqEQx19POBr/lNkZN1AxBy8O9gvDzdpCa2Vn9qahY9eZnDGScrP2KsR34UlXa5PjEMVPtSB4btPi9VOQuRoZImGchfUyf1A2uyIPhV5aC+Zgl60B65WxZ+/nEsVN4NoSgBUv+HlwuRxJPelQKeA9tPwKroqO9PGc5/ee2C1HLH7loD+SwahSPMOY2e8CQd6pLmRF1a/H+ZPWZBW+U7Ekm3YeNNJToUkuonAQB/JbwBvHkZXwsH4/kMHyMPiws5G3nr/jyqF2595KKghIgjGHR1WhGljQzdgO5LT4uuOhesGDRYeMUanvClWSb/mt0SdS8njziY7WoYPYFFFgjRvIIK5FgOd8d2W88I5pj2/SjcXb6GMqEqI3HkCBGPDSo57nSJZzJD8KjJs/4jvzZnGwCFZ8+jeyh562B01mkFfwFaoFOYfqRG3g5sGdZUdY9Nk3FZ8dgEwylUMSxmaL0R2/mzNVasFWp482eHwlK2rae3v+QtCHGfOKn+vsCAwEAAaOCAdIwggHOMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMFYGA1UdIARPME0wQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABATAdBgNVHQ4EFgQUNxW1gjoB4+Qh46Rj3SuULubhtUMwgZkGCCsGAQUFBwEDBIGMMIGJMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCH+SY8KKgw5UDlVL99ToRWPpcloyfOM64UTnNgEDXDDI5r1CNNA0OlggzoEZfakNQJamHjIT287LV7nXFsB4Q9VzyI3H1J5mzVIZrMUiE68wf25BDuA3Zwpri+f8Me78f3nowO2cJ2AiMJ83vQFKKy1LFOixWguuxioKlda2Jq7B57ty5cN+jZwLO7Vrv4Tryg9QeOaxnFvHvuZaxMnE55of7cLpfyAH/5DKvlXx4cdmh7kNO4F/o2LT7om4Cf+Sq6tFS3cUn4zcQbFKT5lw+7vfewzG6X0qYnHbe7Ts/zhh7IJpHnPF1p23ND0+jHgbcDVTFjV4pN1PhVthYHOMeDW461okw2OA/jfuQetUlDwqT5yCdjrOTMDkjZCjTMhcVPzw+7hSUUnewKiR0smuyZbKpE/ZGZWUA6K0sieGCpHGKJo99zD3zmEWmOmq++D0TmVvEiXVJs8fuNWl+VmXSStkMeNR4noHAL1PFUebXVS0lPpQZzBKgqhMGAgbwvYajZnOlvXVll6QashxFZmOVNy88O67s+a2p1SmQTtqNrlodszqkKsc28nDbbvBUd4PUD5tmVgPe29Zwnm1TxFuhl0gqvVc+qZme8zq6yd3nCKNrY6qron4Xcc1rxCWS7NcyO5JiF+qXgAuDOkSFJaaEnQh83ZJsNneXD/nyBH8kSiQ=="; - - public static SessionResult createSessionEndResult() { - SessionResult result = createSessionResult("OK"); - result.setDocumentNumber("PNOEE-31111111111"); - return result; - } - - public static SessionStatus createUserRefusedSessionStatus(String sessionResult) { - SessionStatus status = createCompleteSessionStatus(); - status.setResult(createSessionResult(sessionResult)); - return status; - } - - public static SessionStatus createUserSelectedWrongVerificationCode() { - SessionStatus status = createCompleteSessionStatus(); - status.setResult(createSessionResult("WRONG_VC")); - return status; - } - - public static SessionResult createSessionResult(String endResult) { - SessionResult result = new SessionResult(); - result.setEndResult(endResult); - return result; - } - - public static SessionStatus createCompleteSessionStatus() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - return status; - } -} diff --git a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java deleted file mode 100644 index fabf6b8e..00000000 --- a/src/test/java/ee/sk/smartid/v2/EndpointSslVerificationIntegrationTest.java +++ /dev/null @@ -1,289 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.InputStream; -import java.security.KeyStore; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.v2.integration.SmartIdIntegrationTest; -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; - -@SmartIdDemoIntegrationTest -public class EndpointSslVerificationIntegrationTest { - - private static final String DEMO_HOST_URL = "https://sid.demo.sk.ee/smart-id-rp/v2/"; - private static final String LIVE_HOST_URL = "https://rp-api.smart-id.com/v1"; - private static final String DEMO_RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String DEMO_RELYING_PARTY_NAME = "DEMO"; - private static final String DEMO_DOCUMENT_NUMBER = "PNOLT-30303039914-MOCK-Q"; - - private static final String LIVE_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_live_sk_ee.pem"); - private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); - - @Test - public void makeRequestToDemoApi_useLiveEnvCertificates_sslHandshakeFails() { - var processingException = assertThrows(ProcessingException.class, () -> { - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - - client.setHostUrl(DEMO_HOST_URL); - client.setTrustedCertificates(LIVE_HOST_SSL_CERTIFICATE); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - }); - assertThat(processingException.getMessage(), containsString("unable to find valid certification path to requested target")); - } - - @Test - public void makeRequestToDemoApi_useDemoEnvCertificates_sslHandshakeSuccess() { - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - - client.setHostUrl(DEMO_HOST_URL); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - SmartIdCertificate cert = client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - - assertThat(cert, is(not(nullValue()))); - } - - @Test - public void makeRequestToLiveApi_trustStoreFile() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(LIVE_HOST_URL); - client.setTrustStore(trustStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - }); - } - - @Test - public void makeRequestToLiveApi_trustStoreContext() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - - - SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(trustStore); - trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(LIVE_HOST_URL); - client.setTrustSslContext(trustSslContext); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - }); - } - - @Test - public void makeRequestToDemoApi_provideCustomSSLContext_sslHandshakeSucceeds() { - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - SmartIdCertificate cert = client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - - assertThat(cert, is(not(nullValue()))); - } - - @Test - public void makeRequestToDemoApi_createConfiguredJaxWsClientWithDemoSSLContext_sslHandshakeSucceeds() throws Exception { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(is, "changeit".toCharArray()); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(keyStore); - - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - - Client configuredClient = ClientBuilder.newBuilder().sslContext(sslContext).build(); - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setConfiguredClient(configuredClient); - - SmartIdCertificate cert = client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - - assertThat(cert, is(not(nullValue()))); - } - - @Test - public void makeRequestToDemoApi_loadSslCertificatesFromJksTrustStore_sslHandshakeSucceedsAndCertificateRetrieved() throws Exception { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(keyStore); - - SmartIdCertificate cert = client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - - assertThat(cert, is(not(nullValue()))); - } - - @Test - public void makeRequestToDemoApi_loadSslCertificatesFromPkcs12TrustStore_sslHandshakeSucceedsAndCertificateRetrieved() throws Exception { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.p12"); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(keyStore); - - SmartIdCertificate cert = client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - - assertThat(cert, is(not(nullValue()))); - } - - @Test - public void makeRequestToDemoApi_emptyKeyStore_requestFails() { - assertThrows(ProcessingException.class, () -> { - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(null, null); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(trustStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - }); - } - - @Test - public void makeRequestToDemoApi_loadWrongSslCertificate_requestFails() { - var processingException = assertThrows(ProcessingException.class, () -> { - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/wrong_ssl_cert.jks"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(is, "changeit".toCharArray()); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID(DEMO_RELYING_PARTY_UUID); - client.setRelyingPartyName(DEMO_RELYING_PARTY_NAME); - client.setHostUrl(DEMO_HOST_URL); - client.setTrustStore(keyStore); - - client - .getCertificate() - .withRelyingPartyUUID(DEMO_RELYING_PARTY_UUID) - .withRelyingPartyName(DEMO_RELYING_PARTY_NAME) - .withDocumentNumber(DEMO_DOCUMENT_NUMBER) - .fetch(); - }); - assertThat(processingException.getMessage(), containsString("unable to find valid certification path to requested target")); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java b/src/test/java/ee/sk/smartid/v2/SignableHashTest.java deleted file mode 100644 index 26175bb5..00000000 --- a/src/test/java/ee/sk/smartid/v2/SignableHashTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; - -public class SignableHashTest { - - @Test - public void calculateVerificationCodeWithSha256() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - assertEquals("4240", hashToSign.calculateVerificationCode()); - } - - @Test - public void calculateVerificationCodeWithSha512() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA512); - hashToSign.setHash(DigestCalculator.calculateDigest("Hello World!".getBytes(), HashType.SHA512)); - assertEquals("4664", hashToSign.calculateVerificationCode()); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java b/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java deleted file mode 100644 index 9133130d..00000000 --- a/src/test/java/ee/sk/smartid/v2/SignatureRequestBuilderTest.java +++ /dev/null @@ -1,543 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.v2.DummyData.createSessionEndResult; -import static ee.sk.smartid.v2.DummyData.createUserRefusedSessionStatus; -import static ee.sk.smartid.v2.DummyData.createUserSelectedWrongVerificationCode; -import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Collections; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.v2.rest.SessionStatusPoller; -import ee.sk.smartid.v2.rest.SmartIdConnectorSpy; -import ee.sk.smartid.v2.rest.dao.Capability; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SessionSignature; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -public class SignatureRequestBuilderTest { - - private SmartIdConnectorSpy connector; - private SignatureRequestBuilder builder; - - @BeforeEach - public void setUp() { - connector = new SmartIdConnectorSpy(); - connector.signatureSessionResponseToRespond = createDummySignatureSessionResponse(); - connector.sessionStatusToRespond = createDummySessionStatusResponse(); - builder = new SignatureRequestBuilder(connector, new SessionStatusPoller(connector)); - } - - @Test - public void sign_withHashToSign() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities(Capability.ADVANCED) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Sign hash?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withDataToSign() { - SignableData dataToSign = new SignableData("Say 'hello' to my little friend!".getBytes()); - dataToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableData(dataToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCapabilities("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.verificationCodeChoice("Do you want to say hello?"))) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withoutCertificateLevel() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .sign(); - - assertCorrectSignatureRequestMade(null); - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withShareMdClientIpAddressTrue() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .withShareMdClientIpAddress(true) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - - Assertions.assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - - Assertions.assertTrue(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), - "requestProperties.shareMdClientIpAddress must be true"); - - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void sign_withShareMdClientIpAddressFalse() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - SmartIdSignature signature = builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList(Interaction.confirmationMessageAndVerificationCodeChoice("Sign the contract?"), - Interaction.verificationCodeChoice("Sign hash?"))) - .withShareMdClientIpAddress(false) - .sign(); - - assertCorrectSignatureRequestMade("QUALIFIED"); - - Assertions.assertNotNull(connector.signatureSessionRequestUsed.getRequestProperties(), "getRequestProperties must be set withShareMdClientIpAddress"); - - Assertions.assertFalse(connector.signatureSessionRequestUsed.getRequestProperties().getShareMdClientIpAddress(), - "requestProperties.shareMdClientIpAddress must be false"); - - assertCorrectSessionRequestMade(); - assertSignatureCorrect(signature); - } - - @Test - public void signWithoutDocumentNumber_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .sign(); - }); - assertEquals("Either documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withDocumentNumberAndWithSemanticsIdentifier_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withSemanticsIdentifierAsString("IDCCZ-1234567890") - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .sign(); - }); - assertEquals("Exactly one of documentNumber or semanticsIdentifier must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withoutDataToSign_withoutHash_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, - () -> builder.withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .sign()); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void signWithSignableHash_withoutHashType_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - }); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withHash_withoutHashType_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - }); - assertEquals("Either dataToSign or hash with hashType must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withoutRelyingPartyUuid_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - }); - assertEquals("Parameter relyingPartyUUID must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withoutRelyingPartyName_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .sign(); - }); - assertEquals("Parameter relyingPartyName must be set", smartIdClientException.getMessage()); - } - - @Test - public void sign_withTooLongNonce_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withNonce("THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890") - .sign(); - }); - assertEquals("Nonce cannot be longer that 30 chars. You supplied: 'THIS_IS_LONGER_THAN_ALLOWED_30_CHARS_0123456789012345678901234567890'", smartIdClientException.getMessage()); - } - - - @Test - public void authenticate_displayTextAndPinTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("This text here is longer than 60 characters allowed for displayTextAndPIN")) - ) - .sign(); - }); - assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_verificationCodeChoiceTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.verificationCodeChoice("This text here is longer than 60 characters allowed for verificationCodeChoice")) - ) - .sign(); - }); - assertEquals("displayText60 must not be longer than 60 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_confirmationMessageTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .sign(); - }); - assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); - } - - @Test - public void authenticate_confirmationMessageAndVerificationCodeChoiceTextTooLong_shouldThrowException() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="); - hashToSign.setHashType(HashType.SHA512); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessageAndVerificationCodeChoice("This text here is longer than 200 characters allowed for confirmationMessage. Lorem ipsum dolor sit amet, " + - "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + - "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " + - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - .sign(); - }); - assertEquals("displayText200 must not be longer than 200 characters", smartIdClientException.getMessage()); - } - - - @Test - public void sign_userRefused_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED"); - makeSigningRequest(); - }); - } - - - @Test - public void sign_userRefusedCertChoice_shouldThrowException() { - assertThrows(UserRefusedCertChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CERT_CHOICE"); - makeSigningRequest(); - }); - } - - @Test - public void sign_userRefusedDisplayTextAndPin_shouldThrowException() { - assertThrows(UserRefusedDisplayTextAndPinException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_DISPLAYTEXTANDPIN"); - makeSigningRequest(); - }); - } - - @Test - public void sign_userRefusedVerificationChoice_shouldThrowException() { - assertThrows(UserRefusedVerificationChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_VC_CHOICE"); - makeSigningRequest(); - }); - } - - @Test - public void sign_userRefusedConfirmationMessage_shouldThrowException() { - assertThrows(UserRefusedConfirmationMessageException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE"); - makeSigningRequest(); - }); - } - - @Test - public void sign_userRefusedConfirmationMessageWithVerificationChoice_shouldThrowException() { - assertThrows(UserRefusedConfirmationMessageWithVerificationChoiceException.class, () -> { - connector.sessionStatusToRespond = createUserRefusedSessionStatus("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - makeSigningRequest(); - }); - } - - @Test - public void sign_userSelectedWrongVerificationCode_shouldThrowException() { - assertThrows(UserSelectedWrongVerificationCodeException.class, () -> { - connector.sessionStatusToRespond = createUserSelectedWrongVerificationCode(); - makeSigningRequest(); - }); - } - - @Test - public void sign_signatureMissingInResponse_shouldThrowException() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - connector.sessionStatusToRespond.setSignature(null); - makeSigningRequest(); - }); - assertEquals("Signature was not present in the response", unprocessableSmartIdResponseException.getMessage()); - } - - private void assertCorrectSignatureRequestMade(String expectedCertificateLevel) { - assertEquals("PNOEE-31111111111", connector.documentNumberUsed); - Assertions.assertEquals("relying-party-uuid", connector.signatureSessionRequestUsed.getRelyingPartyUUID()); - Assertions.assertEquals("relying-party-name", connector.signatureSessionRequestUsed.getRelyingPartyName()); - Assertions.assertEquals(expectedCertificateLevel, connector.signatureSessionRequestUsed.getCertificateLevel()); - Assertions.assertEquals("SHA256", connector.signatureSessionRequestUsed.getHashType()); - Assertions.assertEquals("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ=", connector.signatureSessionRequestUsed.getHash()); - } - - private void assertCorrectSessionRequestMade() { - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - } - - private void assertSignatureCorrect(SmartIdSignature signature) { - assertNotNull(signature); - assertEquals("luvjsi1+1iLN9yfDFEh/BE8h", signature.getValueInBase64()); - assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); - assertEquals("PNOEE-31111111111", signature.getDocumentNumber()); - assertThat(signature.getInteractionFlowUsed(), is("verificationCodeChoice")); - } - - private SignatureSessionResponse createDummySignatureSessionResponse() { - SignatureSessionResponse response = new SignatureSessionResponse(); - response.setSessionID("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - return response; - } - - private SessionStatus createDummySessionStatusResponse() { - SessionStatus status = new SessionStatus(); - status.setState("COMPLETE"); - status.setResult(createSessionEndResult()); - SessionSignature signature = new SessionSignature(); - signature.setValue("luvjsi1+1iLN9yfDFEh/BE8h"); - signature.setAlgorithm("sha256WithRSAEncryption"); - status.setSignature(signature); - status.setInteractionFlowUsed("verificationCodeChoice"); - return status; - } - - private void makeSigningRequest() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashInBase64("jsflWgpkVcWOyICotnVn5lazcXdaIWvcvNOWTYPceYQ="); - hashToSign.setHashType(HashType.SHA256); - - builder - .withRelyingPartyUUID("relying-party-uuid") - .withRelyingPartyName("relying-party-name") - .withCertificateLevel("QUALIFIED") - .withSignableHash(hashToSign) - .withDocumentNumber("PNOEE-31111111111") - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Transfer amount X to Y?"))) - .sign(); - } - -} diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java deleted file mode 100644 index accd85b5..00000000 --- a/src/test/java/ee/sk/smartid/v2/SmartIdAuthenticationResponseTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.CertificateEncodingException; - -import org.apache.commons.codec.binary.Base64; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - - -public class SmartIdAuthenticationResponseTest { - - @Test - public void getSignatureValueInBase64() { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ=="); - assertEquals("SGVsbG8gU21hcnQtSUQgc2lnbmF0dXJlIQ==", AuthenticationResponse.getSignatureValueInBase64()); - } - - @Test - public void getSignatureValueInBytes() { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("VGVyZSBhbGxraXJpIQ=="); - assertArrayEquals("Tere allkiri!".getBytes(), AuthenticationResponse.getSignatureValue()); - } - - @Test - public void incorrectBase64StringShouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setSignatureValueInBase64("!IsNotValidBase64Character"); - AuthenticationResponse.getSignatureValue(); - }); - } - - @Test - public void getCertificate() throws CertificateEncodingException { - SmartIdAuthenticationResponse AuthenticationResponse = new SmartIdAuthenticationResponse(); - AuthenticationResponse.setCertificate(CertificateParser.parseX509Certificate(DummyData.CERTIFICATE)); - assertEquals(DummyData.CERTIFICATE, Base64.encodeBase64String(AuthenticationResponse.getCertificate().getEncoded())); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java deleted file mode 100644 index 0ffefa8d..00000000 --- a/src/test/java/ee/sk/smartid/v2/SmartIdClientTest.java +++ /dev/null @@ -1,1191 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubSessionStatusWithState; -import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.oneOf; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.HashType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.SmartIdRestConnector; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -@WireMockTest(httpPort = 18089) -class SmartIdClientTest { - - private SmartIdClient client; - - @BeforeEach - public void setUp() { - client = new SmartIdClient(); - client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - client.setRelyingPartyName("BANK123"); - client.setHostUrl("http://localhost:18089"); - client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E\naWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN\nMjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb\nMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h\ncnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS\nlaHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj\njgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt\n/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO\ndRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan\ngnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38\nyJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN\nRji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX\nMBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW\nMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v\nY3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov\nL2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3\nBglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu\nY29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho\ndHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl\ncnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw\nDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K\ncbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E\noZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd\n0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g\ngw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW\nw+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu\nzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU\n9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1\nfM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2\nFJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg\nlWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t\nYv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu\nulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi\n/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3\ndgE=\n-----END CERTIFICATE-----\n"); - - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequestWithSha512.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequestWithNonce.json", "v2/responses/signatureSessionResponse.json"); - - stubRequestWithResponse("/signature/etsi/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/PASEE-987654321012", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/signature/etsi/IDCEE-AA3456789", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json"); - - stubRequestWithResponse("/authentication/document/PNOEE-31111111111", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PNOEE-31111111111", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/PASEE-987654321012", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/authentication/etsi/IDCEE-AA3456789", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - - stubRequestWithResponse("/certificatechoice/etsi/PASEE-987654321012", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/certificatechoice/etsi/IDCEE-AA3456789", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json"); - } - - @Test - void getCertificateAndSign_fullExample() { - // Provide data bytes to be signed (Default hash type is SHA-512) - var dataToSign = new SignableData("Hello World!".getBytes()); - - // Calculate verification code - assertEquals("4664", dataToSign.calculateVerificationCode()); - - // Get certificate and document number - SmartIdCertificate certificateResponse = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - X509Certificate x509Certificate = certificateResponse.getCertificate(); - String documentNumber = certificateResponse.getDocumentNumber(); - - // Sign the data using the document number - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber(documentNumber) - .withSignableData(dataToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?"))) - .sign(); - - byte[] signatureValue = signature.getValue(); - String algorithmName = signature.getAlgorithmName(); // Returns "sha512WithRSAEncryption" - - String interactionFlowUsed = signature.getInteractionFlowUsed(); - - assertThat(interactionFlowUsed, is(oneOf("displayTextAndPIN", "confirmationMessage"))); - assertValidSignatureCreated(signature); - } - - @Test - void getCertificateAndSign_withExistingHash() { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - String documentNumber = certificateResponse.getDocumentNumber(); - - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber(documentNumber) - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - void getCertificateUsingSemanticsIdentifier() { - var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - SmartIdCertificate certificate = client - .getCertificate() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - void getCertificateUsingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - void getCertificateWithNonce() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-NONCE", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); - - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111-NONCE") - .withCertificateLevel("ADVANCED") - .withNonce("zstOt2umlc") - .fetch(); - - assertCertificateResponseValid(certificate); - } - - @Test - void getCertificateWithManualSessionStatusRequesting() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - - CertificateRequestBuilder builder = client.getCertificate(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); - - assertCertificateResponseValid(certificate); - verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86"))); - } - - @Test - void noTrustStoreOrTrustedCertificates_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - var client = new SmartIdClient(); - client.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - client.setRelyingPartyName("BANK123"); - client.setHostUrl("http://localhost:18089"); - - CertificateRequestBuilder builder = client.getCertificate(); - builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - client.getSmartIdConnector(); - }); - } - - @Test - void getCertificateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111-ADVANCED-LEVEL", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - CertificateRequestBuilder builder = client.getCertificate(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111-ADVANCED-LEVEL") - .withCertificateLevel("ADVANCED") - .initiateCertificateChoice(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdCertificate certificate = builder.createSmartIdCertificate(sessionStatus); - - assertCertificateResponseValid(certificate); - verify(getRequestedFor(urlEqualTo("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86?timeoutMs=5000"))); - } - - @Test - void sign_withDocumentNumber() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - void sign_withSemanticsIdentifier() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789"); - - SmartIdSignature signature = client - .createSignature() - .withSemanticsIdentifier(semanticsIdentifier) - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - void signWithNonce() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SmartIdSignature signature = client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withNonce("zstOt2umlc") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signature); - } - - @Test - void signWithManualSessionStatusRequesting() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - SignatureRequestBuilder builder = client.createSignature(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .initiateSigning(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); - - assertValidSignatureCreated(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00"))); - } - - @Test - void signWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - assertEquals("1796", hashToSign.calculateVerificationCode()); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - SignatureRequestBuilder builder = client.createSignature(); - String sessionId = builder - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .initiateSigning(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdSignature signature = builder.createSmartIdSignature(sessionStatus); - - assertValidSignatureCreated(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=5000"))); - - } - - @Test - void getCertificate_whenUserAccountNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json"); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenUserAccountNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json"); - makeCreateSignatureRequest(); - }); - } - - @Test - void getCertificate_whenUserCancels_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenUserCancels_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); - makeCreateSignatureRequest(); - }); - } - - @Test - void sign_whenTimeout_shouldThrowException() { - assertThrows(SessionTimeoutException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenTimeout.json"); - makeCreateSignatureRequest(); - }); - } - - @Test - void authenticate_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { - assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void sign_whenRequiredInteractionNotSupportedByApp_shouldThrowException() { - assertThrows(RequiredInteractionNotSupportedByAppException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenRequiredInteractionNotSupportedByApp.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void getCertificate_whenDocumentUnusable_shouldThrowException() { - assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenDocumentUnusable.json"); - makeGetCertificateRequest(); - }); - } - - @Test - void getCertificate_whenUnknownErrorCode_shouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - stubRequestWithResponse("/session/97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusWhenUnknownErrorCode.json"); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenDocumentUnusable_shouldThrowException() { - assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusWhenDocumentUnusable.json"); - makeCreateSignatureRequest(); - }); - } - - @Test - void getCertificate_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json"); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json"); - makeCreateSignatureRequest(); - }); - } - - @Test - void getCertificate_whenApiReturnsErrorStatusCode471_shouldThrowNoSuitableAccountOfRequestedTypeFoundException() { - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 471); - makeGetCertificateRequest(); - }); - } - - @Test - void getCertificate_whenApiReturnsErrorStatusCode472_shouldThrowPersonShouldViewSmartIdPortalExceptionn() { - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 472); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", 480); - makeCreateSignatureRequest(); - }); - } - - @Test - void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/certificatechoice/etsi/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", 580); - makeGetCertificateRequest(); - }); - } - - @Test - void sign_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-31111111111", "v2/requests/signatureSessionRequest.json", 580); - makeCreateSignatureRequest(); - }); - } - - @Test - void setPollingSleepTimeoutForSignatureCreation() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureSigningDuration(); - assertTrue(duration > 2000L, "Duration is " + duration); - assertTrue(duration < 3000L, "Duration is " + duration); - } - - @Test - void createSignatureAndGetDeviceIpAddress_noIpAddressReturned() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequest.json", "COMPLETE", STARTED); - SmartIdSignature signature = createSignature(); - - assertThat(signature.getDeviceIpAddress(), is(nullValue())); - } - - @Test - void createSignatureAndGetDeviceIpAddress() { - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("2c52caf4-13b0-41c4-bdc6-aa268403cc00", "v2/responses/sessionStatusForSuccessfulSigningRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); - SmartIdSignature signature = createSignature(); - - assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); - assertThat(signature.getDeviceIpAddress(), is("62.65.42.46")); - } - - @Test - void setPollingSleepTimeoutForCertificateChoice() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-31111111111", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("97f5058e-e308-4c83-ac14-7712b0eb9d86", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureCertificateChoiceDuration(); - assertTrue(duration > 2000L, "Duration is " + duration); - assertTrue(duration < 3000L, "Duration is " + duration); - } - - @Test - void setSessionStatusResponseSocketTimeout() { - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SmartIdSignature signature = createSignature(); - assertNotNull(signature); - verify(getRequestedFor(urlEqualTo("/session/2c52caf4-13b0-41c4-bdc6-aa268403cc00?timeoutMs=10000"))); - } - - @Test - void authenticateUsingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - void authenticate_usingSemanticsIdentifier() { - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withSemanticsIdentifierAsString("PNOEE-31111111111") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - void authenticateWithNonce() { - stubRequestWithResponse("/authentication/document/PNOEE-31111111111-WITH-NONCE", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); - - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOEE-31111111111-WITH-NONCE") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withNonce("g9rp4kjca3") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authenticationResponse); - } - - @Test - void authenticateWithManualSessionStatusRequesting() { - var semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111"); - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - AuthenticationRequestBuilder builder = client.createAuthentication(); - String sessionId = builder - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .initiateAuthentication(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); - - assertAuthenticationResponseValid(authenticationResponse); - verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb"))); - } - - @Test - void authenticateWithManualSessionStatusRequesting_andCustomResponseSocketTimeout() { - var semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111"); - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - assertEquals("4430", authenticationHash.calculateVerificationCode()); - - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5); - AuthenticationRequestBuilder builder = client.createAuthentication(); - String sessionId = builder - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .initiateAuthentication(); - - SessionStatus sessionStatus = client.getSmartIdConnector().getSessionStatus(sessionId); - SmartIdAuthenticationResponse authenticationResponse = builder.createSmartIdAuthenticationResponse(sessionStatus); - - assertAuthenticationResponseValid(authenticationResponse); - verify(getRequestedFor(urlEqualTo("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb?timeoutMs=5000"))); - } - - @Test - void authenticate_whenUserAccountNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenUserCancels_shouldThrowException() { - assertThrows(UserRefusedException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenUserRefusedGeneral.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenTimeout_shouldThrowException() { - assertThrows(SessionTimeoutException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenTimeout.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenDocumentUnusable_shouldThrowException() { - assertThrows(DocumentUnusableException.class, () -> { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - stubRequestWithResponse("/session/1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusWhenDocumentUnusable.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json"); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", 480); - makeAuthenticationRequest(); - }); - } - - @Test - void authenticate_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", 580); - makeAuthenticationRequest(); - }); - } - - @Test - void setPollingSleepTimeoutForAuthentication() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); - client.setPollingSleepTimeout(TimeUnit.SECONDS, 2L); - long duration = measureAuthenticationDuration(); - assertTrue(duration > 2000L, "Duration is " + duration); - assertTrue(duration < 3000L, "Duration is " + duration); - } - - - @Test - void getDeviceIpAddress_ipAddressNotPresent() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequest.json", "COMPLETE", STARTED); - - SmartIdAuthenticationResponse authentication = createAuthentication(); - assertThat(authentication.getDeviceIpAddress(), is(nullValue())); - } - - @Test - void getDeviceIpAddress_ipAddressReturned() { - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusRunning.json", STARTED, "COMPLETE"); - stubSessionStatusWithState("1dcc1600-29a6-4e95-a95c-d69b31febcfb", "v2/responses/sessionStatusForSuccessfulAuthenticationRequestWithDeviceIpAddress.json", "COMPLETE", STARTED); - - SmartIdAuthenticationResponse authentication = createAuthentication(); - assertThat(authentication.getDeviceIpAddress(), is("62.65.42.45")); - } - - @Test - void verifyAuthentication_withNetworkConnectionConfigurationHavingCustomHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-32222222222-Z1B2-Q", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - - String headerName = "custom-header"; - String headerValue = "Hi!"; - - Map headersToAdd = new HashMap<>(); - headersToAdd.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headersToAdd); - client.setNetworkConnectionConfig(clientConfig); - makeAuthenticationRequest(); - - verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-32222222222-Z1B2-Q")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void verifySigning_withNetworkConnectionConfigurationHavingCustomHeader() { - String headerName = "custom-header"; - String headerValue = "Hello?!"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); - client.setNetworkConnectionConfig(clientConfig); - makeCreateSignatureRequest(); - - verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-31111111111")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void verifyCertificateChoice_withNetworkConnectionConfigurationHavingCustomHeader() { - String headerName = "custom-header"; - String headerValue = "Man, come on.."; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - ClientConfig clientConfig = getClientConfigWithCustomRequestHeaders(headers); - client.setNetworkConnectionConfig(clientConfig); - makeGetCertificateRequest(); - - verify(postRequestedFor(urlEqualTo("/certificatechoice/etsi/PNOEE-31111111111")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void verifySmartIdConnector_whenConnectorIsNotProvided() { - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - assertInstanceOf(SmartIdRestConnector.class, smartIdConnector); - } - - @Test - void verifySmartIdConnector_whenConnectorIsProvided() { - final String mock = "MOCK"; - SessionStatus status = mock(SessionStatus.class); - when(status.getState()).thenReturn(mock); - SmartIdConnector connector = mock(SmartIdConnector.class); - when(connector.getSessionStatus(null)).thenReturn(status); - client.setSmartIdConnector(connector); - assertEquals(mock, client.getSmartIdConnector().getSessionStatus(null).getState()); - } - - @Test - void getCertificate_noIdentifierGiven() { - assertThrows(SmartIdClientException.class, () -> - client - .getCertificate() - .withCertificateLevel("ADVANCED") - .fetch() - ); - } - - @Test - void getCertificateByETSIPNO_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - void getCertificateByETSIPAS_ValidSemanticsIdentifierAsString_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - void getCertificateByETSIIDC_ValidSemanticsIdentifier_ShouldReturnValidCertificate() { - SmartIdCertificate cer = client - .getCertificate() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .fetch(); - - assertCertificateResponseValid(cer); - } - - @Test - void getAuthenticationByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - void getAuthenticationByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - void getAuthenticationByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulAuthentication() { - - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - SmartIdAuthenticationResponse authResponse = client - .createAuthentication() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .withAuthenticationHash(authenticationHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - - assertAuthenticationResponseValid(authResponse); - } - - @Test - void getSignatureByETSIPNO_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - var signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - @Test - void getSignatureByETSIPAS_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - var signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "987654321012")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - @Test - void getSignatureByETSIIDC_ValidSemanticsIdentifier_ShouldReturnSuccessfulSignature() { - - var signableHash = new SignableHash(); - signableHash.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - signableHash.setHashType(HashType.SHA256); - - SmartIdSignature signResponse = client - .createSignature() - .withSemanticsIdentifier( - new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.EE, "AA3456789")) - .withCertificateLevel("ADVANCED") - .withSignableHash(signableHash) - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - - assertValidSignatureCreated(signResponse); - } - - private long measureSigningDuration() { - long startTime = System.currentTimeMillis(); - SmartIdSignature signature = createSignature(); - long endTime = System.currentTimeMillis(); - assertNotNull(signature); - return endTime - startTime; - } - - private SmartIdSignature createSignature() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - return client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - } - - private long measureAuthenticationDuration() { - long startTime = System.currentTimeMillis(); - SmartIdAuthenticationResponse AuthenticationResponse = createAuthentication(); - long endTime = System.currentTimeMillis(); - assertNotNull(AuthenticationResponse); - return endTime - startTime; - } - - private SmartIdAuthenticationResponse createAuthentication() { - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - return client - .createAuthentication() - .withDocumentNumber("PNOEE-31111111111") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - } - - private long measureCertificateChoiceDuration() { - long startTime = System.currentTimeMillis(); - SmartIdCertificate certificate = client - .getCertificate() - .withDocumentNumber("PNOEE-31111111111") - .withCertificateLevel("ADVANCED") - .fetch(); - long endTime = System.currentTimeMillis(); - assertNotNull(certificate); - return endTime - startTime; - } - - private void makeGetCertificateRequest() { - client - .getCertificate() - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.EE, "31111111111")) - .withCertificateLevel("ADVANCED") - .fetch(); - } - - private void makeCreateSignatureRequest() { - var hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - client - .createSignature() - .withDocumentNumber("PNOEE-31111111111") - .withSignableHash(hashToSign) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ) - .sign(); - } - - private void makeAuthenticationRequest() { - var authenticationHash = new AuthenticationHash(); - authenticationHash.setHashInBase64("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - authenticationHash.setHashType(HashType.SHA512); - - client - .createAuthentication() - .withDocumentNumber("PNOEE-32222222222-Z1B2-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("ADVANCED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ) - .authenticate(); - } - - private ClientConfig getClientConfigWithCustomRequestHeaders(Map headers) { - var clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); - clientConfig.register(new ClientRequestHeaderFilter(headers)); - return clientConfig; - } - - private void assertCertificateResponseValid(SmartIdCertificate certificate) { - assertNotNull(certificate); - assertNotNull(certificate.getCertificate()); - String name = certificate.getCertificate().getSubjectX500Principal().getName(); - assertThat(getAttribute(name, BCStyle.SERIALNUMBER), is("PNOEE-31111111111")); - assertEquals("PNOEE-31111111111", certificate.getDocumentNumber()); - assertEquals("QUALIFIED", certificate.getCertificateLevel()); - } - - private void assertValidSignatureCreated(SmartIdSignature signature) { - assertNotNull(signature); - assertThat(signature.getValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); - assertEquals("sha256WithRSAEncryption", signature.getAlgorithmName()); - assertThat(signature.getInteractionFlowUsed(), is("displayTextAndPIN")); - } - - private void assertAuthenticationResponseValid(SmartIdAuthenticationResponse authenticationResponse) { - assertNotNull(authenticationResponse); - assertEquals("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ==", authenticationResponse.getSignedHashInBase64()); - assertEquals("OK", authenticationResponse.getEndResult()); - assertNotNull(authenticationResponse.getCertificate()); - assertThat(authenticationResponse.getSignatureValueInBase64(), startsWith("luvjsi1+1iLN9yfDFEh/BE8h")); - assertEquals("sha256WithRSAEncryption", authenticationResponse.getAlgorithmName()); - assertEquals("PNOEE-31111111111", authenticationResponse.getDocumentNumber()); - } - - private static String getAttribute(String name, ASN1ObjectIdentifier oid) { - var x500name = new X500Name(name); - RDN[] rdns = x500name.getRDNs(oid); - return IETFUtils.valueToString(rdns[0].getFirst().getValue()); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java b/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java deleted file mode 100644 index 06ef4dd7..00000000 --- a/src/test/java/ee/sk/smartid/v2/SmartIdSignatureTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package ee.sk.smartid.v2; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -public class SmartIdSignatureTest { - - @Test - public void getSignatureValueInBase64() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("VGVyZSBNYWFpbG0="); - assertEquals("VGVyZSBNYWFpbG0=", signature.getValueInBase64()); - } - - @Test - public void getSignatureValueInBytes() { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("RGVkZ2Vob2c="); - assertArrayEquals("Dedgehog".getBytes(), signature.getValue()); - } - - @Test - public void incorrectBase64StringShouldThrowException() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - SmartIdSignature signature = new SmartIdSignature(); - signature.setValueInBase64("äIsNotValidBase64Character"); - signature.getValue(); - }); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java b/src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java deleted file mode 100644 index 17c97610..00000000 --- a/src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java +++ /dev/null @@ -1,706 +0,0 @@ -package ee.sk.smartid.v2.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.assertAuthenticationResponseCreated; -import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.createAuthenticationSessionRequest; -import static ee.sk.smartid.v2.rest.SmartIdRestIntegrationTest.pollSessionStatus; -import static java.util.Arrays.asList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.InputStream; -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Arrays; -import java.util.Collections; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import org.apache.http.client.config.RequestConfig; -import org.glassfish.jersey.apache.connector.ApacheClientProperties; -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.hamcrest.CoreMatchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.CertificateParser; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.HashType; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.AuthenticationHash; -import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.v2.SignableHash; -import ee.sk.smartid.v2.SmartIdAuthenticationResponse; -import ee.sk.smartid.v2.SmartIdCertificate; -import ee.sk.smartid.v2.SmartIdClient; -import ee.sk.smartid.v2.SmartIdSignature; -import ee.sk.smartid.v2.rest.SmartIdConnector; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.InteractionFlow; -import ee.sk.smartid.v2.rest.dao.SessionStatus; - -/** - * These tests contain snippets used in Readme.md - * This is needed to guarantee that tests compile. - * If anything changes in this class (except setUp method) the changes must be reflected in Readme.md - * These are not real tests! - */ -@SmartIdDemoIntegrationTest -public class ReadmeTest { - private static final Logger logger = LoggerFactory.getLogger(ReadmeTest.class); - - private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); - - private SmartIdClient client; - - private SmartIdAuthenticationResponse authenticationResponse; - - private SignableHash hashToSign; - - @BeforeEach - public void setUp() { - client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - authenticationResponse = new SmartIdAuthenticationResponse(); - - hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - // calculate hash from the document you want to sign (i.e. use DigiDoc4J or other libraries) - // this class also has a method to set hash as bite array - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - } - - /* - - ## COPY THIS TO END OF README.MD - - - - ## Example of configuring the client - - You need a client for any call to API. - - The production environment host URL, relying party UUID and name are fixed in the Smart-ID service agreement. - - ### Verifying the SSL connection to Application Provider (SK) - - Relying Party needs to verify that it is connecting to Smart-ID API it trusts. - More info about this requirement can be found from [Smart-ID Documentation](https://github.com/SK-EID/smart-id-documentation#35-api-endpoint-authentication). - - #### Reading trusted certificates from key store - -It is recommended to read trusted certificates from a file. - - - */ - - - @Test - public void documentConfigureTheClient_trustStore() throws Exception { - // reading trusted certificates from external trustStore file - InputStream is = SmartIdIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks"); - KeyStore trustStore = KeyStore.getInstance("JKS"); - trustStore.load(is, "changeit".toCharArray()); - - // Client setup. Note that these values are demo environment specific. - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setTrustStore(trustStore); - } - - /* - - ### Feeding trusted certificates one by one - - It is also possible to feed trusted certificates one by one. - This can prove useful when trusted certificates are kept as application configuration property. - - */ - - - @Test - public void documentConfigureTheClient_feedSeparately() { - assertThrows(SmartIdClientException.class, () -> { - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setTrustedCertificates( - "-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj...", - "-----BEGIN CERTIFICATE-----\nMIIE0zCCA7ugAwIBAgIQbQr/Ky22GFhYWS3oQoJkyT..." - ); - }); - } - - /* - - ## Examples of performing authentication - - ### Authenticating with semantics identifier - - More info about Semantics Identifier can be found: https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf - - */ - - @Test - public void documentAuthenticatingWithSemanticsIdentifier() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, // 3 character identity type (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code - "30303039903"); // identifier (according to country and identity type reference) - - // For security reasons a new hash value must be created for each new authentication request - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - String verificationCode = authenticationHash.calculateVerificationCode(); - - // NB! Display verification code to the customer for a few seconds before starting next step: - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") // Certificate level can either be "QUALIFIED" or "ADVANCED" - // Smart-ID app will display verification code to the user and user must insert PIN1 - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("Log in to self-service?") - )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option - .withShareMdClientIpAddress(true) - .authenticate(); - - // You need this if you want to implement signing - String documentNumberForFurtherReference = authenticationResponse.getDocumentNumber(); - - // We get IP of Smart-ID app since we made the request .withShareMdClientIpAddress(true) - String deviceIpAddress = authenticationResponse.getDeviceIpAddress(); - } - - /* - - Note that verificationCode should be displayed by the web service, so the person signing through the Smart-ID mobile app can verify - if the verification code displayed on the phone matches with the one shown on the web page. - Leave a few seconds for the verification code to be displayed for users using the web service with their mobile device. - Then start the authentication process (which triggers Smart-ID app in the phone which covers the verification code displayed). - - ### Authenticating with document number - - If you already know the documentNumber you can use this for (re-)authentication. - Each document number is connected with specific mobile device of user. - If user has Smart-ID installed to multiple devices then this triggers notification to a specific device only. - This is why it is recommended to use authentication with document number if you want to target specific device only. - - */ - - - @Test - public void documentAuthenticatingWithDocumentNumber() { - - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - - String verificationCode = authenticationHash.calculateVerificationCode(); - - // NB! Display verification code to the customer for a few seconds before starting next step: - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withDocumentNumber("PNOEE-30303039903-MOCK-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - // Smart-ID app will show 3 different verification codes to user and user must choose correct verification code - // before the user can enter PIN. If user selects wrong verification code then the operation will fail. - Interaction.verificationCodeChoice("Log in to self-service?") - )) - .authenticate(); - } - - /* - - ## Validating authentication response - - It is mandatory to validate the authentication response. - Validation performs following checks: - - - "signature.value" is the valid signature over the same "hash", which was submitted by the RP. - - "signature.value" is the valid signature, verifiable with the public key inside the certificate of the user, given in the field "cert.value" - - The person's certificate given in the "cert.value" is valid (not expired, signed by trusted CA and with correct (i.e. the same as in response structure, greater than or equal to that in the original request) level). - - The identity of the authenticated person is in the 'subject' field of the included X.509 certificate. - - */ - - @Test - public void documentAuthValidation() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> { - // init Authentication response validator with trusted certificates loaded from within library - // as an alternative you can pass trusted certificates array as parameter to constructor - AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); - - // throws SmartIdResponseValidationException if validation doesn't pass - AuthenticationIdentity authIdentity = authenticationResponseValidator.validate(authenticationResponse); - - String givenName = authIdentity.getGivenName(); // e.g. Mari-Liis" - String surname = authIdentity.getSurname(); // e.g. "Männik" - String identityCode = authIdentity.getIdentityNumber(); // e.g. "47101010033" - String country = authIdentity.getCountry(); // e.g. "EE", "LV", "LT" - Optional birthDate = authIdentity.getDateOfBirth(); // see next paragraph - - - /** - * ### Extracting date-of-birth - * Since all Estonian and Lithuanian national identity numbers contain date-of-birth - * this function always returns a correct value for them. - *

      - * For persons with Latvian national identity number the date-of-birth is parsed - * from a separate field but for some old Smart-id accounts the value might be missing. - *

      - * More info about the availability of the separate field in certificates: - * https://github.com/SK-EID/smart-id-documentation/wiki/FAQ#where-can-i-find-users-date-of-birth - */ - - Optional dateOfBirth = authIdentity.getDateOfBirth(); - - /** - One can also only fetch the signing certificate of a person - and then construct authentication identity from that - and extract the date-of-birth from there. - */ - - // skip these lines in readme.md - String certificate = "MIIIojCCBoqgAwIBAgIQJ5zu8nauSO5hSFPXGPNAtzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIwMDkyNjQ3WhcNMjQwOTIwMDkyNjQ3WjBlMQswCQYDVQQGEwJFRTEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEaMBgGA1UEBRMRUE5PRUUtMzk5MTIzMTk5OTcwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCI0y7aO3TlSbLgVRCGYmWZsiSg5U9ZIFjIBxQL9j6kYGUJZ+bGtyEmxXBj7KleqbueTqeZEEfzSPhtHuyPWuT4r7KfPl427/oKUpWcIrHWbLzLDFVAj4k9U2zN4vAAviTcVd6Qp/7ADsQgMAJFOktCfmLA82MHgWEh2E9jIL15I0HDbi5fuhWMv6FpUWJ/b4dZAzZjGvx9FMmoMw8OzHFc8JjfvsfaZ3DOlR/hGikFgeexEHt96mkmsnHO2vge/EHaggksIQg6OWubNodS+LN0MVvQCvNTFmBMyiHelSEiL/zDVxFoVQUc4WJmn+8i6nhTUq8C6uO+LvngIN22dUEfRn0+v2A9Yo/cuevPgMSFGFmJZL3sY1WCjdGPeku7uBq7S2H8nd37VhkPrKhfDUgMs1PP7aK3ESfNgW9gL/nlfYaWv/jMOaewEylQM+LUPJvVlpfAPRt4wOt6ZcJcS3t+NwQmGprtjtl8iWeQe3bfq35uVvvqBL/aA/CswhugXwLADKGYWhQa408FN4NRCuUFAVzi2foWjOP8MVE+ayR527+PcKykVBKn9JoNaPje7nigSoJLzXqRaz47QE2u8jFHEhVjwMwAwVQenaqQvEU0eWKdstIwoa9xOPNFMxFXkFrsuuyt22hIeRLN/nrxTMQnbwvmH7eQlM2bR6mA8ik5BJu4fzvsQsExsSxcX3WBfZc56/J1zizWoFMJ8+LOyqlZ6gPhVDzaFtEDOpT1C8m3GucpZQxSP0iJRr4XMYXKU8v3SDByYyCM9K1S/m9tZUOpjsHBX5xDrUXKdRXfrtk7qQJGngfEjSaQ12nweQgDIEpuIHoJ6m9yrOOMQa1CBJQGytHKBeXOB/nqF5IxzI5RTtrzEFLiqKqB+iFnPkA5PMsSCOGgAqGxg+of5eQtxIU7xgEeft7JxPnoDly5ohcnvip8/yAEptDgwJQybbEsbM4a+qjGkMz1O7ZrhptJR3VpppV7IIaLu/kxru7akHMuNXabYF+Sv3OzxhbRgTePT18CAwEAAaOCAkkwggJFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFPw86wO2tJOrY1RPmQeyY9TfaAf8MIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS0zOTkxMjMxOTk5Ny1BQUFBLVEwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTAzMDMwMzEyMDAwMFowDQYJKoZIhvcNAQELBQADggIBACQZH/fgKOUowei48VVlXJWLfxvyXTYKsp7SnS/VwtOj+y7IOQkTa+ZbHM27A5bhd+Bz1iruI5TSb3R2ZLF9U4KNXHbywaa7cAEimzXEMozeDvNdTkpawzTnCVih44iLCYdZ0GGRi6Wn6/Ue6EltN3hIucYPuzAO9dhwFrVSuTyaNSVKSi6TW/1jONNCX4+/XktcArArnarH5l+rfPQgecXYFvZ5xwywvFLrKXG1qUBtgH+3OrSsY4OtLiE56iCwMWGk/zpKa2ZSGPol8WmJIrHMEVR1jxUTMaEJLAEpiXbA2LH7+Js7/JPtbhbsyQGDjib4nNlle/ai29tKvX5cyccw1tCi7/KzcqwMI+Wy6fi6fVjdKFqI/bl3ouO7kqUO7STI+9xN6usMw+3Kb08FvX1ak8pDfiYod3iJ7Ky9+G8gLBxjApWB3ZfHn4aMz5SdaJBiuZvjk5kDbDk47wK/DuN+QkmXDWhftUsRbyNNHGT0M+qgbMzQ6b9OB6uZ957SfoB96vKUIN0oZ1ZSHpjMSqqlEv6wZO8+bmU6Bk3VqPDgBWvuJeztTdz+ylXhwx5TtClCSv0mw6bEcHJsOlgRyGu2XtGD0ILtfypfZNTzVtP9kqiKIXA+TkKtqfyR6ifry3kddJuqQ/swrpFb+/msYh367B1Rxca6ucgtfo2hKPQL"; - X509Certificate x509Certificate = CertificateParser.parseX509Certificate(certificate); - // skip previous 2 lines from readme.md - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(x509Certificate); - Optional signersCertificate = identity.getDateOfBirth(); - - assertThat(signersCertificate, CoreMatchers.is(LocalDate.of(1903, 3, 3))); - - // skip that: - }); - } - - - - /* - - ## Creating a signature - - ### Obtaining signer's certificate - - To create a digital signature, most format require the signer's certificate beforehand. - To fetch the certificate you can use documentNumber. - - */ - - @Test - public void documentObtainingUsersCertificate() { - - SmartIdCertificate responseWithSigningCertificate = client - .getCertificate() - .withDocumentNumber("PNOEE-30303039903-MOCK-Q") // returned as authentication result - .withCertificateLevel("QUALIFIED") - .fetch(); - - - X509Certificate signersCertificate = responseWithSigningCertificate.getCertificate(); - - } - - /* - - If needed you can use semantics identifier instead of document number to obtain signer's certificate. - This may trigger a notification to all the user's devices if user has more than one device with Smart-ID - (as each device has separate signing certificate). - - - ### Create the signature - - All Smart-ID devices support displaying text that is up to 60 characters long. - Some devices also support displaying text (on a separate screen) that is up to 200 characters long - as well as other interaction flows like user needs to choose the correct code from 3 different verification codes. - - You can send different interactions to user's device and it picks the first one that the app can handle. - -You need to use other utilities (like [DigiDoc4j](https://github.com/open-eid/digidoc4j) for example) to -create the AsicE/BDoc container with files in it and get the hash to be signed. - */ - - - @Test - public void documentCreatingSignature() { - SignableHash hashToSign = new SignableHash(); - hashToSign.setHashType(HashType.SHA256); - // calculate hash from the document you want to sign (i.e. use Digidoc4J or other libraries) - // this class also has a method to set hash as bite array - hashToSign.setHashInBase64("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - - // to display the verification code - String verificationCode = hashToSign.calculateVerificationCode(); - - // pause for a few seconds before starting following signing process - - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // returned as authentication result - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here."), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - - byte[] signature = smartIdSignature.getValue(); - - String usedFlow = smartIdSignature.getInteractionFlowUsed();// which interaction was used - - } - - /* - -# Setting the order of preferred interactions for displaying text and asking PIN - -The app can support different interaction flows and a Relying Party can demand a particular flow with or without a fallback possibility. -Different interaction flows can support different amount of data to display information to user. - -Available interactions: -* `displayTextAndPIN` with `displayText60`. The simplest interaction with max 60 chars of text and PIN entry on a single screen. Every app has this interaction available. -* `verificationCodeChoice` with `displayText60`. On first screen user must choose the correct verification code that was displayed to him from 3 verification codes. Then second screen is displayed with max 60 chars text and PIN input. -* `confirmationMessage` with `displayText200`. The first screen is for text only (max 200 chars) and has the Confirm and Cancel buttons. The second screen is for a PIN. -* `confirmationMessageAndVerificationCodeChoice` with `displayText200`. First screen combines text and Verification Code choice. Second screen is for PIN. - -RP uses `allowedInteractionsOrder` parameter to list interactions it allows for the current transaction. Not all app versions can support all interactions though. -The Smart-ID server is aware of which app installations support which interactions. When processing Replying Party request the first interaction supported by the app is taken from `allowedInteractionsOrder` list and sent to client. -The interaction that was actually used is reported back to RP with interactionFlowUsed response parameter to the session request. -If the app cannot support any interaction requested the session is cancelled and client throws exception `RequiredInteractionNotSupportedByAppException`. - -`displayText60`, `displayText200` - Text to display for authentication consent dialog on the mobile device. Limited to 60 and 200 characters respectively. - -## Parameter allowedInteractionsOrder most common examples - -Following allowedInteractionsOrder combinations are most likely to be used. - -### Short confirmation message with PIN - -If confirmation message fits to 60 characters then this is the most common choice. -Every Smart-ID app supports this interaction flow and there is no need to provide any fallbacks to this interaction. - -*/ - @Test - public void documentInteractionOrderMostCommon() { - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.displayTextAndPIN("My confirmation message that is no more than 60 chars") - )) - .sign(); - } - - /* -### Verification code choice - -This is more secure than previous example as the app forces user to look up the verification code displayed to him and -pick the same verification code from 3 different codes displayed in Smart-ID app and thus tries to assure that user is not interacting with some other service. - -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. - -If user's app doesn't support displaying verification code choice then system falls back to displaying text and PIN input. - - */ - - - @Test - public void documentInteractionOrderVerificationChoice() { - try { - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Arrays.asList( - Interaction.verificationCodeChoice("My confirmation message that is no more than 60 chars"), - Interaction.displayTextAndPIN("My confirmation message that is no more than 60 chars") - )) - .sign(); - } catch (UserSelectedWrongVerificationCodeException wrongVerificationCodeException) { - System.out.println("User selected wrong verification code from 3-code choice"); - } - } - - /* - - -### Long confirmation message with fallback to PIN - -Relying Party first choice is confirmationMessage that can be up to 200 characters long. -If the Smart-ID app in user's smart device doesn't support this feature then the app falls back to displayTextAndPIN interaction. - -*/ - - @Test - public void documentInteractionOrderConfirmationWithFallbackToPin() { - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") // - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here."), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - - if (InteractionFlow.CONFIRMATION_MESSAGE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app was able to display full text to user"); - } else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text to user"); - } - - } - -/* -### Long confirmation message together with verification code choice with fallback to verification code choice. - -Relying Party first choice is confirmationMessage followed by verification code choice. -If this is not available then only verification code choice with shorter text is displayed. - -If user picks wrong verification code then the session is cancelled and library throws `UserSelectedWrongVerificationCodeException`. - -*/ - - @Test - public void documentInteractionOrder2() { - SmartIdSignature smartIdSignature = client - .createSignature() - .withDocumentNumber("PNOEE-30303039903-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Long text (up to 200 characters) goes here."), - Interaction.verificationCodeChoice("Shorter text for less capable devices"), - Interaction.displayTextAndPIN("Shorter text for less capable devices") - )) - .sign(); - - if (InteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app was able to display full text on separate screen and verification code choice."); - } else if (InteractionFlow.VERIFICATION_CODE_CHOICE.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text together with verification choice."); - } else if (InteractionFlow.DISPLAY_TEXT_AND_PIN.is(smartIdSignature.getInteractionFlowUsed())) { - System.out.println("Smart-ID app displayed shorter text to user with PIN input."); - } - } - - /* - -### Listing interactions with longer text without fallback - -Relying Party can require interactions without fallback. -If End User's phone doesn't support required flow the library throws `RequiredInteractionNotSupportedByAppException`. - - - */ - @Test - public void documentInteractionOrderWithoutFallback() { - try { - client - .createSignature() - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withSignableHash(hashToSign) - .withCertificateLevel("QUALIFIED") - .withAllowedInteractionsOrder(Collections.singletonList( - Interaction.confirmationMessage("Long text (up to 200 characters) goes here.") - )) - .sign(); - } catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("User's Smart-ID app is not capable of displaying required interaction"); - } - } - - - /* - ## Network connection configuration of the client - -Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: - -- Initiation request -- Session status request - -Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. - - */ - - @Test - public void documentClientTimeoutConfig() { - SmartIdClient client = new SmartIdClient(); - // ... - // sets the timeout for each session status poll - client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); - // sets the pause between each session status poll - client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); - } - - /* - - As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. - - Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: - - */ - - @Test - public void documentClientConnectionTimeoutConfig() { - SmartIdClient client = new SmartIdClient(); - // ... - ClientConfig clientConfig = new ClientConfig(); - clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); - clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); - - client.setNetworkConnectionConfig(clientConfig); - - } - - /* - - And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: - - */ - @Test - public void documentApacheHttpClient() { - SmartIdClient client = new SmartIdClient(); - // ... - ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); - RequestConfig reqConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(30000) - .setConnectionRequestTimeout(5000) - .build(); - clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); - - client.setNetworkConnectionConfig(clientConfig); - } - - @Test - @Disabled("you need to run a proxy to run this test") - public void document_setProxy_withJbossRestEasy() throws Exception { - // in order to run this test you can set up a proxy server locally - //docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta - - - // CODE EXAMPLE STARTS HERE - - org.jboss.resteasy.client.jaxrs.ResteasyClient resteasyClient = - new org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl() - .defaultProxy("127.0.0.1", 3128, "http") - .build(); - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setConfiguredClient(resteasyClient); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - // CODE EXAMPLE ENDS HERE - - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); - - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), smartIdConnector); - assertAuthenticationResponseCreated(sessionStatus); - } - - @Test - @Disabled("you need a running proxy server to run this test") - public void document_setNetworkConnectionConfig_withJersey() throws Exception { - // in order to run this test you first have to set up a proxy server locally - //docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta - - // CODE EXAMPLE STARTS HERE - - org.glassfish.jersey.client.ClientConfig clientConfig = - new org.glassfish.jersey.client.ClientConfig(); - clientConfig.property(ClientProperties.PROXY_URI, "http://127.0.0.1:3128"); - - SmartIdClient client = new SmartIdClient(); - client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - client.setRelyingPartyName("DEMO"); - client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v2/"); - client.setNetworkConnectionConfig(clientConfig); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - - // CODE EXAMPLE ENDS HERE - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "010906-29990"); - - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - SmartIdConnector smartIdConnector = client.getSmartIdConnector(); - AuthenticationSessionResponse authenticationSessionResponse = smartIdConnector.authenticate(semanticsIdentifier, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), smartIdConnector); - assertAuthenticationResponseCreated(sessionStatus); - } - -} diff --git a/src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java deleted file mode 100644 index cfdc3782..00000000 --- a/src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java +++ /dev/null @@ -1,268 +0,0 @@ -package ee.sk.smartid.v2.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.security.cert.CertificateEncodingException; -import java.time.LocalDate; -import java.util.Collections; - -import org.apache.commons.codec.binary.Base64; -import org.hamcrest.CoreMatchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.AuthenticationHash; -import ee.sk.smartid.v2.AuthenticationResponseValidator; -import ee.sk.smartid.v2.SignableData; -import ee.sk.smartid.v2.SmartIdAuthenticationResponse; -import ee.sk.smartid.v2.SmartIdCertificate; -import ee.sk.smartid.v2.SmartIdClient; -import ee.sk.smartid.v2.SmartIdSignature; -import ee.sk.smartid.v2.rest.dao.Interaction; - -@SmartIdDemoIntegrationTest -public class SmartIdIntegrationTest { - - private static final String HOST_URL = "https://sid.demo.sk.ee/smart-id-rp/v2/"; - private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String DATA_TO_SIGN = "Well hello there!"; - private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; - private static final String CERTIFICATE_LEVEL_ADVANCED = "ADVANCED"; - - private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); - - private SmartIdClient client; - - @BeforeEach - public void setUp() { - client = new SmartIdClient(); - client.setRelyingPartyUUID(RELYING_PARTY_UUID); - client.setRelyingPartyName(RELYING_PARTY_NAME); - client.setHostUrl(HOST_URL); - client.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - } - - @Test - public void getCertificate_bySemanticsIdentifier() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LT, "30303039914")) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039914-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIeDCCBmCgAwIBAgIQcnLdjYj7nH5m/WBe9hNIpjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTUwMTUwWhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkxUMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk5MTQwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCYPFgri+lor5RVPUHuUHbLiHZFJ82WijgayMc1Bnj/fKQxOlq5DWX73Tozuebbw96+1t9qTX3zek2uYt+PZ6pedo0ZF5JNmti+zTgBqF+/KvLoUB9Kas51NYugKfRJDx38GXXRG/rpWI6PiumrDEaoLLi7eMfShZT49Bl5CxeZbTWhMttt/TJQ2KTJG4rVLXam8N8cXm3oQt1SA1e7Ceiz1Xx9y45HEbQovufYB8/YQDnp+wDzFb1lN1A6K/RBmSxKrqXXNjxkFHgaBkZ1YzdWM6NcvB9cFsSCU4w9FBLkcvpYprc09TuFok4xNnxn86hjdMEZBUQhE10CODGHzmSKD+KFHULSx+b0FccGjMaFQ0/79rau2+YjOGHF+yoC9bAg6XtmPZk68ZBK2AHm0bC1zzYsyzbqWh+gLq41fGzZBFvbxzaXwW42oShf8+47fV0zKZfqBC9q8Arg32wLJY0kr0k/lkGtAO+rVuok3wwH0ncddKP+OHbR2IgTicsz2xgnF+8ItqRSgJ5yoNuMWUzNd7NBGTActryA5cydHfAZv/61702jEqz4CdaNPWu6evvv18wFkys0M9BKjFjPcHbXDxXp5N3/XbGRHyu88p/dNWebx4HoDX5LepifQYxJ9OjTTP1BJAv1LdnFyN4juzEPxyO+CbIB5oqsuxUqhUXR5AB1F9vkLGANuejPSqyKLV8qVYbBGQK2sefqUw9LwUFrPh4sV+Pz4uI3bA0uDA2r42MFkExMk/XV57yINSdJUG1E75NrFIMNbuMUZp2cbmKIuIrupXn1tGRGQhsjlpGkdBvonGHQZPdzlIrOG4qFEJB72uBHqeIlJpfeBkdSC7f9BqDB5mkKuVZ8Fj55lWCU2xkzJ2UxBeyCfRElFSBCwo/AlAxI2fGCZkjl5JIZ0rslG6rBm21cDuaLfspYifizzFJ0mGsJ+iqtU/eh2KxRcbRKRj1GMkOWS3E1tiriohvjoxQG5xF+u8s/ht5TP62YQfG5Dkl++T7wOEnnGMGsr5UCAwEAAaOCAh8wggIbMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFPem4JAsN+0DrjswFiQ8ZYejemZcMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9MVC0zMDMwMzAzOTkxNC1NT0NLLVEwDQYJKoZIhvcNAQELBQADggIBAJ8yCBedb80PyP8YOFUZ4g+CkZNtXjWJ+2F6+2p9qfotHxYbJCQ36PSuq8nD+9+VNSXKLINStxHSasCmDoX62/IRf38tXCXHBba9h3gi2Cw5Q5oINV7WaMLQohU5MU88udNDYWvVcho7wEOkJ0EkXR4pEnOhtrol8hwAbNU2iP8jAuq3YocwyayEzMBm7CE9T2hMAf3H2TzydM7dMLmwu5/HDX/GjqpKBMXNeJPhW3L9FVJVdGhkBKiSyaXAqui46t32OkYO2useovah+yNX43Xvc4/ESBeA07pgJH7ATO0KyFcfV5CRVgq1WUm1NL69wP7OAEX/T1QhCiAJcJaxIzIGsgFmqbFLP9Q0+KaFSdFW0ZEWkDNmaThXXVm7dGY9FP90DOvqgr36thT9wrZBdZid+fsljBa7gxc92GUiGJ9f1t0F2uHJRNYzMdldApr1uh6hwH/VNy3U7uKdT7VLmJikK6GAHEbUR9ZQIfKBvllN7nyhfK90HUnAB0FfdG4RYyCaZGeKi7mJxGxeJGzkQB/GnWHTmcKasKHWJKolXFV/HdQt2sI7VUDdRgFs3JwADeBWnRCEv/DCaStvHndcsxzzV7ZjvVyC3COjx/jeldfBqiywgGQu0bPOqJJ0p5aYtDjly5cOEpGWKhVO04O6B2DvxfxyfKOg2s/FOcHnf9Gq")); - } - - @Test - public void getCertificate_bySemanticsIdentifier_latvian() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012")) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-030303-10012-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIejCCBmKgAwIBAgIQSMxs7XxO4eBm/WFlTLHbUDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTUwNjEzWhgPMjAzMDEyMTcyMzU5NTlaMGQxCzAJBgNVBAYTAkxWMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEbMBkGA1UEBRMSUE5PTFYtMDMwMzAzLTEwMDEyMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAminatsgmv6e/jO+OQy1upDQq6MfolQ35cJVSlPnDHH/AOYgG8Qd96lqxYWbYsTx0A3anP6H9t7ctO+Ur6CrO08SxQm2NwBudCYcoseaBd0+KzHPKAs5rTzwQenrsMYVVdwrvE0scB6t50bIHBwyHYxevJSOuyQ1qUrIQJu/jzrNTnKrmZuBTi2310otePc/pYPy8UsqG9EUHiRdlsK6HnfdoHJa8BgJgIPbTOPWS0PGy8Q3cZxw3EjHmQLZmtAY+QK78PwYIS1I9xrP+RUI9o2piyvt/D1EuEwwBXRLrvdEtrWCOdYnSyhT0+8tWiKg2AIBtb8zOOEQ2df4VUqCFcXa/Pr9a3d+Xx3Ikj2tVxjfPteJ1RO89/CMi65klT01y5nGn0mTpW6T0YMSbQFI2bckVnem0VpucK7qsAGqSnI9QYGNGbn50Y0J5ZF+sbKJh5GLnr65vwyhfgzKlgHx3Ag/fMIT1gXdvLQyIFlj9clt8H93aaCV4lHPs5jmRaXxVkQbshaUCNFTFNAZY9kM1OHlbtmnem9aZCrEUfaLbJ1Fh1mCbVZLmZs3qPDErW29MqXy0qpiFkLKiF7EaieuiSsVcWkel/BCzsgqWoR+vX0RV5ydhoM1rNaOssXKO1zu6YQyluElOv3zIomStU6s9Hy7x3i+6Qi1u32fKc7oK45uzagh642xu3ziY3BFeAoJAHooVFpGxo13a3wekzwsmJfA/CJfVnAZUCwP1QmO4R0XWOb8UBDMQqGpsyroSbn+QqutGb9xCNx8cN0qHBUkzrwz1eWfWADx8cdLlfn06OorGP0ORyKGPRtPk03lfrDwzZoFky7lxoXIUqVj1xfB9R5aVtS/e61gWu38hNbe7TKOcHz2/SL6stctEUvcDtmRoSwj+kN8DbFUOg7xPvmwVmuJqIVtPQFDPKFmkjwzaOoilI3oLwcJwCXGHTWVTvnH25bQSLEpR6urZnQMipHXLGGyXMXUtwT06oajWIGq4MMY19QM43b24c1pIgGR9WKjHAgMBAAGjggIgMIICHDAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTjDtjZcvT0VXcGijoC8vVFuptWJTCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMDMwMzAzLTEwMDEyLU1PQ0stUTANBgkqhkiG9w0BAQsFAAOCAgEAnosMKGxxp7mNkvFwCrYcn4anl2QBGVyLuQ5IgE/IgER5/H4XKXmh7mSl+XzgODx2LYSd9ZqQLtYnDek16m1A0e5wBYyxjditQaG481/qOl+mOPSP8cmpEyYaImve3jHU0jZbdyehGWX2DGdda67asng6qADLvTWOCHFyIkCErLXOLEIR+0NqhOQqBrJKAa4Iy/+HhhFexkvQn6IWbduW3sUQdmyHIpXuOyXRS17+AUdpwISWF99DeZCwuJw/yGdygbRCu3J2UmzT/7pOtETJsw/2KgYFl0PF7YLar/rTPxwSc0X00tDp5Bx8Uzh97c6Ytsy+joVQdjWzhXUa0tYkQKKlfCshItwl5OXnidTf9axJdt4eINk6hSLWjlJ9+AvSVsTa7fH3CrIzz+ueADKgHY3sDvtfv1S3E3rGQSt2tss3gIxWGv0jAmq7a+XCfK4G4ypKRf1Fh6rWloDw8GjDcqlStnLLXt9ytAe4hoaZEna1g1KpMJZvzJGX2ejb/wzN2PFNgF3sqdSTlwhox0AMjBW2bjRHa92iq7qCXV6r+Od6KQVOAPcKv/aPNml1sdqGwx/w1qsj1u/amHe4XfPhQxQX81GFqm0yBgrNwmaZIoM2S0dUNA/Vu9/oQ4PmEzTvCxBtLdLD5nsJTUWnl0Guj19Z7L3A+7TLpHk3XLIgIRc=")); - } - - @Test - public void getCertificate_bySemanticsIdentifier_dateOfBirthParsedFromFieldInCertificate() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "329999-99901")) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); // - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLV-329999-99901-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIpjCCBo6gAwIBAgIQbFI0PFmHC9ZkMOYfyerrLDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjMwNDA4MDM1NzE4WhgPMjAzMDEyMTcyMzU5NTlaMGYxCzAJBgNVBAYTAkxWMRcwFQYDVQQDDA5URVNUTlVNQkVSLEJPRDETMBEGA1UEBAwKVEVTVE5VTUJFUjEMMAoGA1UEKgwDQk9EMRswGQYDVQQFExJQTk9MVi0zMjk5OTktOTk5MDEwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCMARH2ne8GpokAEdARSdBytebXr4xcL7Kbw8PYZ/NDDp0oArreJ8D5XT8hJP+ceuYuqGFGNu4drPmckctE3w86zeTaORXlDIwLXIVmQ5sCvnAo0G9QTxeVFQmDIwfY29VrRAlbkb+OGaXvDnOyn5U96wajr9yxXKUT6224Uh15Y3cL1UHMxpbUep1bEvPnguOwBri2oT43aHzIVe+ydBcYcTwd7zoXwp3g0dFmxjZVNOIA21LVy2dEVGkbUnIZCtHbOXrYG/i3s0tYoKQ6gVAdFuzPX9fDSJc0ftBdgo6tRhucipulsfHHE6pjlbHzWuLroL/dWjPhuX1wbauBdnfwwL3CDjNpRtavvPUw7o6nfuX3OBb3i2APIuxWGpAKjVCOTlS+TNK5TsYh8NDBaE38Dgur7qNFAcp5MnHuSwISIE4McSyIlpu4/SY/n0Fl0xcYlFvHW28hjsbfvECoF1oIXgYHyBnZPo+OD7BXYvW/Bz6MTb9CsRshwKpPz9wd7J8I0jMVZ9gUI89qk2iQBnxxEDOkq3w25HDfz/iMnQHnnnPXOBAsDfx/N46bXFdyxl4naFQSb1lTo+5jeP1fCnwaCvF+d5kq9Nz+YV987UCGZlldvNfmSW1iZ06a9ZSaN8zPww8v/30WaFKrPNbkCPhev4yVjzK0x0q0KmuCCsbyN7Q9tJWP2nCDqQAJ0gg9lTwrzvBOgqfpQ4TtJCN60Q74Mdkp+lf53xxV1xMSsDYiA4voncxtXf812cmWSUpuErSTdV4ns3AgoYv4IpsUgAMKMZCb9e36heIGRYWiqiLmDQnX7w1YR9gmeenvb2XW8VaEJ5xjE7s14dgFf58110757LSiUs9wD8PUWCflFEzDbUJbzBVy+Myc9jcWkFrZtER0ljUp+agYIf+OoQ63mZAB1keiiRFQiSfK6V8c1HkojIGzwGfdYbF8vnwoFpHd4Gme0gkihhUgNeyh5CH87P+TfQGOzszz4wLHNkniZqgXry+pnQOlYGzR/KSxsju/M3ECAwEAAaOCAkowggJGMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgZAMF0GA1UdIARWMFQwRwYKKwYBBAHOHwMRAjA5MDcGCCsGAQUFBwIBFitodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9DUFMvMAkGBwQAi+xAAQIwHQYDVR0OBBYEFHGqapCMVIwyoExCFHG5q2lF8MXxMIGuBggrBgEFBQcBAwSBoTCBnjAIBgYEAI5GAQEwFQYIKwYBBQUHCwIwCQYHBACL7EkBATATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3NraWRzb2x1dGlvbnMuZXUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0zMjk5OTktOTk5MDEtTU9DSy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTk5OTEyMzExMjAwMDBaMA0GCSqGSIb3DQEBCwUAA4ICAQDket8XOYpGKYqPLJb7qW5WPslTtzH141kbusWmsA1j+7rwaC5u857VpMVM/2B28DUfXUgd/QC4kxU5q8TfqB0QoOxd2tb36liLszRpInlc2BAJ+dJ10dpTJ7EvYdD1TpQXQxmUYslwg6NyCFnsVn2jCYW71rSIcOV5/FgHnJtxypLy97atPmsAwbC+LlsjXL5CckbwAg5Xnw3PBoqfpWPe11jyA4hBE8tl2Lzi/mMhQzdvB6UB+wBcdRHxIcE2LI5G5Rf08ddesCHFn3GznHLrtnxuJIW6gNiNkxP6eCwpp8Y6X28TWqLSEXsROjcnMyv3acpVAGxDBFnt6rwJRvjfcPDNNOfCCjWQaD1ReSZtjhzK95ycO0YqYGrDRYNuajmLmLJyXA0TNqKgOHNgmzDSZpmpYXU6b1hUdyC9PmOAJ69pdtFSZxYaCMFjo6sgKN+pwosOB01rODsAoeqfPbRPGWuON6tYhvtaDkNVPaLtz6BWoAJX1d9luZ2PKi6eX3TpffpH0YprnXhXweBJGeY2WGga8fKAXyoutgJbS9m/PUrpjdxQYJ4sxd75QXi4XnhDVST9fyM6q4ustLS118BMixiMz1BYbK1nLUshOF+KWZG3wUOqSNj4dDnmi+9ZV0u+xerCKfneBKtYymqDFVyUv9j3noZYzEub2uZMED2B5w==")); - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); - assertThat(identity.getDateOfBirth().isPresent(), CoreMatchers.is(true)); - assertThat(identity.getDateOfBirth().get(), CoreMatchers.is(LocalDate.of(1999, 12, 31))); - } - - @Test - public void getCertificate_EstonianByDocumentNumber_dateOfBirthParsedFromFieldInCertificate() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOEE-40404049996-MOCK-Q") - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); - - - assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-40404049996-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIoTCCBomgAwIBAgIQDnRWtLc1cm9jj2SA/ncFwzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjIxMjA2MTU0OTE5WhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkVFMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PRUUtNDA0MDQwNDk5OTYwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAHTW5RQN6eA/Iu51xFsFGJKyepBpovEzZ33XfvzJUbuNlsaQC/gEGZqkSG1NqcLx00AJXyxWiWXfwv5PGYYZoS4MVLFacUT/WkiI/cth6PevslhDVYxITooCYMhirmimKHvPd01XVzbGpvO498zW3qetLsv/FZcQyNV0Xh4JTVPEk05j6nQSZNh5dHSBzvLe41fzKPCw+N5KV3Szr3+Ov0i00jNbdV5kHgqSCvbr46iWrnew8MTO+Se6O4LatlZkAocwIQgpuYmvGL/ThhUHws4uVyKFHpdFsxdBA3BD4PpsXp3g4we3FNl2ZCj9W/o25jY3kryHcGZimE2iYa/139kpu+RggXZDQlQ+R6/p6ClM2W53hAtcr0HnZ+VEhMZ88MQTjvgqntyrMVbFqYrkpmlC5CPYhO5UDrUS6VFnv46iKP69QddWSkFQMUvjg7YDCGwFWtagYhRLK2hjTc3bF6CAV436SnDasY67RIFJrIrYnRbj0lv8SPph6nv/+khXwYp/DeF9xriuy69tPtoFlA3LxCeqPMMrUNgY3o/GcNqVh0TrUB0671DR9jmTrjl1dWfie6xdyO255MHWptBO1wys85LKNuy822DS0tdQLOZHsGXSNYCJUn0//9eeAMApX1a720G/C6qwyRf/wX1N1qhPJgMpTCFaWxfgmjFjYPnw7JjP+cCqZyIIH4+PPirLu1awVtcuPtTEHDEkUWnELKouXSltw8OpcblIs8ocVdfSy0Mil+09yz1fawi2zgulfLOj8I/liJo8c9KFvwOotFYRf2qVV8VuLM4OS1ucSLIH+fp2PtnyjyZOy1+2J0KlrxHRrTTejLRS/i4fkq+VWg2hIoAsYgpwgRNPqN7jvdaguaQcqyc9E8ht+w9pWep/SexC9bCKaDp8GUHu9ft9emoJQOOLB4RtI+O6V4arC8T3UbelL9u4zodKpUJiC2GTl8U6IrKjMSYqNObCbRM+fwF83/VP6WEK71EN3S9kFWRnGYE/bamIEaIBte3bc9cuIQIDAQABo4ICSTCCAkUwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwXQYDVR0gBFYwVDBHBgorBgEEAc4fAxECMDkwNwYIKwYBBQUHAgEWK2h0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABAjAdBgNVHQ4EFgQUaiwzCeEb6XKZ5WlgUMZj5/7264wwga4GCCsGAQUFBwEDBIGhMIGeMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfAYIKwYBBQUHAQEEcDBuMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBBBggrBgEFBQcwAoY1aHR0cDovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VJRC1TS18yMDE2LmRlci5jcnQwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNDA0MDQ5OTk2LU1PQ0stUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDQwNDA0MTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAFdJJqV/lvpVU489Ti0//cgynwgTE99wAVBpArgd8rD8apVMBoEn+Tu0Lez5YnfbK6+Dx1WvdM4t74xxkUlXkMIXLJI6iYM6mDiueDTvF94k51f1UWQo+/0GVO+dIDE1gmIm5K3eV/J7+/duSkrA72VHNJGCd8HVnj2UUOvo5VLBfQi7WjGjhff8LBXINUnBHIfs6CXrDJiLPwQQy/5pv03maJOG+isPT/IrhnkYBgOWDKaPCAkAvaGDaAPJGVNpu4QijuqKEzKrW9AGpmf1WxPhnp63zWOiEYuPhuqUnKH2IqG9gThi2l23zKU/7EbxOLd1vrElqAyHLvLS/PgSgiR/XxBUotxceeXYtnL20NxfzuYdEM1gz8UFyix4M5L905j/5Yuwksq/QN0c1A3gFQtHhtVrlSxzQpipd967HJezJxdsh6VlxuI0r6MSzcDOYVkOo3oE1sV/kyHtnhdWAVOh9u3EVtXBPyfWOMcPiloIDTJhbQ0pJFRLgEELSlYwObDzeqtRXMmtNpilK3feKu98PQekaQp1xv4dHyMIUsKLxNgyhGtV9o1mWoGpFaQImsF8jDeP2XckzmWh7s33SDm1/O4BgyyXbMNOa3HjP6l8LKb341M2lQAGs6JjelwIkOOUGYKr56SYshueeC92Xd/kOUY+pTCFQ87krYpBFETk=")); - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); - assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1904, 4, 4))); - - } - - @Test - public void getCertificate_LithuanianByDocumentNumber_dateOfBirthParsedFromFieldInCertificate() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOLT-30303039816-MOCK-Q") - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withNonce("012345678901234567890123456789") - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOLT-30303039816-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIiTCCBnGgAwIBAgIQc6PEd785oXRm/SM0Oc1W1zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQxMDAyMTA0MDUyWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkxUMR8wHQYDVQQDDBZURVNUTlVNQkVSLE1VTFRJUExFIE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMRQwEgYDVQQqDAtNVUxUSVBMRSBPSzEaMBgGA1UEBRMRUE5PTFQtMzAzMDMwMzk4MTYwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAGWxALQGbmmDLrkdOFl8z0cUsCrGooykaSPBGQ6UhpT3rJZv9OsepcBS4WkGrWDfF70tyRZZ0HHDIjmae9fQBZ9eABkweWLhW76QxV8sFzUXD3hm4HByYI5OZwyTXAgXg6ZJvLioXl/4L+SYwCJI6/sOBkNpzzwbuOKVe3zYB4QlMzfgzE7GqBDjM621dk0KqE8lTaKrAiIxK+x+zWfXiCzll8+Fmqwfd2bjD67fzg+qsqS3AuwI5MoS7myaEil6ZeZnlEO35oXU5kyYjx7auKgmSp908vXmvQCvDs93lYU2WqYG+QRfoSrjF78JYzSZqEgibkX+uqZzIIyHGe/JRe4P0A4qRItoZR6MAl8v3a3tKkWHqfavFhmvuTdRKSHpJR27J5D8uhI1sTFMx8p/W0nITH7xFK6KQRee3AJYuwi/VhS3h52bdbkMsVnGzFA0MRImqeC0qA2TbRwm3ZyzAMpHoI5ESgw22SzBK6/15kz/fqEuLpIRUfZZg54+Vj+cNSvaLVosHfdXsJB0yow3VfV+o7PkH+UYVBzd52H1WusmQC3AWBb1hurlFCvbZbNr6AD5RJ/NLPsTc7Gl1Mt7rtEyM4Ov8HyTa4lWNvUc4VPm3lQDOU46kCFLruUlFd3phP9eZx61AznjaNW/MQd0kTV8/juhgooEOcn/sm4pnPu6JEJBHgZX4vIZa3ekCL/q6VRXGvQK5yADUqNNHpmkVuPUqc1g0vUZjtxZ7eNxtnJfz6/NkZqRsY8ONxKOcf/w6zT+crPopFvdK+fep0PXzU/7cVz5oeEzf8CdlAEidRyWLs4ZfUOYMbyvXmeCnmwkH5CxhNy0F3kfmoAf/iBN6L/w1p5NHoa1nNmeBjp2vu5ADnMxPP/lxUz5ToxZb+mIOKGsdGXNVKFnPWCyI6d6FtvnMqDyfhqjPEPeGfAxfNU3KJFWn7EE9EIplTCB9Tm6wSezTUi7dAevWJb4+rAfwQ1fwXYwMq9ZrYV+uKqsDKnzp4q/8DY9SoLMP/7tIA4FKQIDAQABo4ICHzCCAhswCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwXQYDVR0gBFYwVDBHBgorBgEEAc4fAxECMDkwNwYIKwYBBQUHAgEWK2h0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L0NQUy8wCQYHBACL7EABAjAdBgNVHQ4EFgQUyhobJizCQFZeBpnDkNJI0+Fka5kwga4GCCsGAQUFBwEDBIGhMIGeMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vc2tpZHNvbHV0aW9ucy5ldS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtmVf46HQK/ErQwfAYIKwYBBQUHAQEEcDBuMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBBBggrBgEFBQcwAoY1aHR0cDovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VJRC1TS18yMDE2LmRlci5jcnQwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0xULTMwMzAzMDM5ODE2LU1PQ0stUTANBgkqhkiG9w0BAQsFAAOCAgEALLxANwHBOLkkQzeHzV8APzCEo5LLky5Ha6ovodZ3WDsbIblxjMoq/BXw6AIkUD8zoq1pXFTvTR3ggzW6N8YAkDs+wtgg49P6bp7j96Tb5kFeYdleuqcRD/3r5IThqIpMCjFmjQDP3547Tbf4ELIIR9yvglgTw5we6V4CfrJ0RqET+V+7j1VmH+zg/S1E9lyaUOWE/5u9njWryj/ftmSqCJDTvzLigj2gOjiTWagzdGzQoRYXwuc5wefD+t5cLWHSumPkQidVUcPp7BUK3gaIBuXHMvnPN9sCTrXIg/AWwAzMUJzU94EN2cWb+k/EHYHIVsQ7j1qHS8cxvB6j+i4F4GbmmJQeKYEMGI6MCiMwlqVnNMzECaG2DQo7Rg9qQDY+bAxQDqtHoIeUY1Fvuwl4mQtyfsmUP4x7+B5AlKWB0lFUZ3Txez+HhGglA9+PYt1lHfpRmQMZzbaUvt2RDwp9f27vBqME+YKm6MFcJjDt5h+HfMosIAkZQOZbXc7EsTvEqtCcFwi9THAAZqAoG4LWqkLHDshov0ME8OlBre0OenKdexEav/ECSNFmxQG6o4tNDYA4tBRPb9Z7bEE6CPb/iFm34eBqFXy4uRVZkt5DBjhCZHXr9DxCoKj7UykptrtpY28sw/J67YJK3ci6pQQwzRfMgXZCWnxOCLNMYk0G9EA=")); - - AuthenticationIdentity identity = AuthenticationResponseValidator.constructAuthenticationIdentity(certificateResponse.getCertificate()); - assertThat(identity.getDateOfBirth().orElse(null), CoreMatchers.is(LocalDate.of(1903, 3, 3))); - } - - @Test - public void getCertificateEE_byDocumentNumber() throws CertificateEncodingException { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOEE-30303039914-MOCK-Q") - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .fetch(); - - assertThat(certificateResponse.getDocumentNumber(), is("PNOEE-30303039914-MOCK-Q")); - assertThat(certificateResponse.getCertificateLevel(), is("QUALIFIED")); - assertThat(Base64.encodeBase64String(certificateResponse.getCertificate().getEncoded()), is("MIIIvjCCBqagAwIBAgIQTyHvSQrAdk9mgogev6nM4DANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMjQwNzAxMTA0MjM4WhgPMjAzMDEyMTcyMzU5NTlaMGMxCzAJBgNVBAYTAkVFMRYwFAYDVQQDDA1URVNUTlVNQkVSLE9LMRMwEQYDVQQEDApURVNUTlVNQkVSMQswCQYDVQQqDAJPSzEaMBgGA1UEBRMRUE5PRUUtMzAzMDMwMzk5MTQwggMhMA0GCSqGSIb3DQEBAQUAA4IDDgAwggMJAoIDAGlRrnunxGGVyekLA/sTMgCNAZ4KX7vR1BtK7RlAfsfxK+7dWevL1yuTkSs67xIgYUhaRQhck3f6Px2izcDzWHZy7Z7h8CzuZBR8YjQzQmfzIxurInXCtP0vmU4lUMfHg0w2OMHoSYOCmPEh5H+57BCom/zwHxciW92pRZeovyyj1qXEdtdHbDks2peKScNz+rcImv/53eTO2wPkjno5yzTfsbammlBLG1IYaA5VRh3YGFwfRVBMdRoV92Ym7x7vDh9LiZXfoK6+lF5OQ+wwBVjEYVqAlVaTxmtmXiY4mcekTZ+cr5mULOJ7QnD7+NRSSjBGLwEPVTweNWnX8D6bJq2VeZWoKahPoqwbv6g9dda5Hu+6fX36ed88h5A8VT58743BqITQ4fA10EpF5uC6a2+9V1BA1UQMUtlBGGZweof9bW3nE7EWSR5f3U+koW/pBrGFUsnpBSCXWP3NqWzSggrYOqY2ErrBoE8FxEKsHU140X+8SWtxxLc0UyYGn21hEBk66YbvwqeSApNRh3x/aQMyJtNCoNEEGxc8vgAHuiQePUPLbTeFl+cfyvrWANTzssMqJStfH5LunAzLyh3qgZ+qe8qwUUl9PMIKsM8fIYMHQUAFZddKzLrQz/iiDvKYwmG2iRZx7GgM8ubN1LKP+ld9+YTXQ/aUJnQQjjgCe/ySuiq8U1rDe77J/hB6S+UD268hGYCSo1FXkda/ekVbvnCvl5CGmHUp9NRcmjfthvWffdy+CK1F7ctPBHDVbak3/SZcJkcQaNerKmuTWOdGjpw+pZkqrDdVaDWGmb8XtRccGxeir99CaNqJfN4CTov/CqV899Pk70LUYw/+8grViREozX8chofFEBUAeIRzJgyAdIteDlj9lzCEFlqXDN8MjfepSDVpF0po7BAs7tyrUoJ+BjIRdKBS3kH87iMZhUtxtevhhP/P9xV0lLqlPuRWjr5if7kL9U3WPz/ws/5O517IiBSktvgInCZ4I/+gSka7wJvsuDD3fMSqfGERTOtrTwIDAQABo4ICZjCCAmIwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwegYDVR0gBHMwcTBkBgorBgEEAc4fAxECMFYwVAYIKwYBBQUHAgEWSGh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NlcnRpZmljYXRpb24tcHJhY3RpY2Utc3RhdGVtZW50LzAJBgcEAIvsQAECMB0GA1UdDgQWBBRjqdXLjzqj0aJobR82+uqO5xYhHjCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAwBgNVHREEKTAnpCUwIzEhMB8GA1UEAwwYUE5PRUUtMzAzMDMwMzk5MTQtTU9DSy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwMzAzMDMxMjAwMDBaMA0GCSqGSIb3DQEBCwUAA4ICAQDqZEspnUbRmbHUzWVbeSaoVgDqZmRNGu8BP+p/fP5T8NvVRUbinVZ3dq+6/+hhUxyKUDppNYzWYoNa4Zdamf16z4yvPVwy/z3D3EthNX4ePJQSHVk6zQKjBk9/4Hu4u+9EEsxJW/z1F7DHkfL94/KCG8fOybIK9uwfMQLFVgH4gvT4CWMXlxkw7R6PI2rnfrCGtjzLo536aJ23i4smns/wL83G+g92jy/zXD3PyuOlHgEWXaWoLdG+pKdRVTus4ppkbfLsaGTvSEVLvcwHhIWVOi0TkeNDjQpKe92yNHEIFc1SRq4I4h9bbgAWZRDy77R3UDmRrDZGlg2WBZ+tdmo0saqi0fdhihAx5PzupltNLbamJu3yz/HBZvbUw+Dz2ADfCTNT9cf+o1iaeFtpPy/iPSepZtAWmQXfmtPezgFliFMSs0q5tGVRcai2+PrRrvrzIT6+5aeSsfCkk1SlgfLeYp1P6Fc2yI5MoMux2y01sFhJhpuhDf3nh0IE4JJS6awulr6GXw41ipCopCWt4oJsfXCFJLZdhxwpMtlrqZ21qY7dGbV85X012o+78CFGpbRh5wBC0cwi1hDhq/aM6FZZHY0Y+S3NVEDnbb6oz2muLksBvXZiz+8iSWHT2bReUYMRTnM3XXtEjUvhHYqIJiMqgAqbAJ2ttRtVrHj2He4p7Q==")); - } - - - @Test - public void getCertificateAndSignHash_withValidRelayingPartyAndUser_successfulCertificateRequestAndDataSigning() { - SmartIdCertificate certificateResponse = client - .getCertificate() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOLT-30303039914-MOCK-Q") - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .fetch(); - - assertCertificateChosen(certificateResponse); - - String documentNumber = certificateResponse.getDocumentNumber(); - SignableData dataToSign = new SignableData(DATA_TO_SIGN.getBytes()); - - SmartIdSignature signature = client - .createSignature() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber(documentNumber) - .withSignableData(dataToSign) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withAllowedInteractionsOrder( - Collections.singletonList(Interaction.displayTextAndPIN("012345678901234567890123456789012345678901234567890123456789")) - ) - .sign(); - - assertSignatureCreated(signature); - } - - @Test - public void authenticate_withValidUserAndRelayingPartyAndHash_successfulAuthentication() { - AuthenticationHash authenticationHash = AuthenticationHash.generateRandomHash(); - assertNotNull(authenticationHash.calculateVerificationCode()); - - SmartIdAuthenticationResponse authenticationResponse = client - .createAuthentication() - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withDocumentNumber("PNOLT-40404049996-MOCK-Q") - .withAuthenticationHash(authenticationHash) - .withCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to internet bank?"))) - .withShareMdClientIpAddress(true) - .authenticate(); - - assertAuthenticationResponseCreated(authenticationResponse, authenticationHash.getHashInBase64()); - - AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(authenticationResponse); - - assertThat(authenticationIdentity.getGivenName(), is("OK")); - assertThat(authenticationIdentity.getSurname(), is("TESTNUMBER")); - assertThat(authenticationIdentity.getIdentityNumber(), is("40404049996")); - assertThat(authenticationIdentity.getCountry(), is("LT")); - - System.out.println("Device IP: " + authenticationResponse.getDeviceIpAddress()); - } - - private void assertSignatureCreated(SmartIdSignature signature) { - assertNotNull(signature); - assertThat(signature.getValueInBase64(), not(emptyOrNullString())); - } - - private void assertCertificateChosen(SmartIdCertificate certificateResponse) { - assertNotNull(certificateResponse); - assertThat(certificateResponse.getDocumentNumber(), not(emptyOrNullString())); - assertNotNull(certificateResponse.getCertificate()); - } - - private void assertAuthenticationResponseCreated(SmartIdAuthenticationResponse authenticationResponse, String expectedHashToSignInBase64) { - assertNotNull(authenticationResponse); - assertThat(authenticationResponse.getEndResult(), not(emptyOrNullString())); - assertEquals(expectedHashToSignInBase64, authenticationResponse.getSignedHashInBase64()); - assertThat(authenticationResponse.getSignatureValueInBase64(), not(emptyOrNullString())); - assertNotNull(authenticationResponse.getCertificate()); - assertNotNull(authenticationResponse.getCertificateLevel()); - } - -} diff --git a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java deleted file mode 100644 index 08918b4a..00000000 --- a/src/test/java/ee/sk/smartid/v2/rest/SessionStatusPollerTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.v2.DummyData.createSessionEndResult; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SessionStatusRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -public class SessionStatusPollerTest { - - private SmartIdConnectorStub connector; - private SessionStatusPoller poller; - - @BeforeEach - public void setUp() { - connector = new SmartIdConnectorStub(); - poller = new SessionStatusPoller(connector); - poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 1L); - } - - @Test - public void getFirstCompleteResponse() { - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", connector.sessionIdUsed); - assertEquals(1, connector.responseNumber); - assertCompleteStateReceived(status); - } - - @Test - public void pollAndGetThirdCompleteResponse() { - connector.responses.add(createRunningSessionStatus()); - connector.responses.add(createRunningSessionStatus()); - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertEquals(3, connector.responseNumber); - assertCompleteStateReceived(status); - } - - @Test - public void setPollingSleepTime() { - poller.setPollingSleepTime(TimeUnit.MILLISECONDS, 200L); - addMultipleRunningSessionResponses(5); - connector.responses.add(createCompleteSessionStatus()); - long duration = measurePollingDuration(); - assertThat(duration, is(greaterThanOrEqualTo(1000L))); - assertThat(duration, is(lessThanOrEqualTo(1500L))); - } - - @Test - public void setResponseSocketOpenTime() { - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.MINUTES, 2L); - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertCompleteStateReceived(status); - assertTrue(connector.requestUsed.isResponseSocketOpenTimeSet()); - assertEquals(TimeUnit.MINUTES, connector.requestUsed.getResponseSocketOpenTimeUnit()); - assertEquals(2L, connector.requestUsed.getResponseSocketOpenTimeValue()); - } - - @Test - public void responseSocketOpenTimeShouldNotBeSetByDefault() { - connector.responses.add(createCompleteSessionStatus()); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - assertCompleteStateReceived(status); - assertFalse(connector.requestUsed.isResponseSocketOpenTimeSet()); - } - - private long measurePollingDuration() { - long startTime = System.currentTimeMillis(); - SessionStatus status = poller.fetchFinalSessionStatus("97f5058e-e308-4c83-ac14-7712b0eb9d86"); - long endTime = System.currentTimeMillis(); - assertCompleteStateReceived(status); - return endTime - startTime; - } - - private void addMultipleRunningSessionResponses(int numberOfResponses) { - for (int i = 0; i < numberOfResponses; i++) { - connector.responses.add(createRunningSessionStatus()); - } - } - - private void assertCompleteStateReceived(SessionStatus status) { - assertNotNull(status); - assertEquals("COMPLETE", status.getState()); - } - - private SessionStatus createCompleteSessionStatus() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(createSessionEndResult()); - return sessionStatus; - } - - private SessionStatus createRunningSessionStatus() { - SessionStatus status = new SessionStatus(); - status.setState("RUNNING"); - return status; - } - - public static class SmartIdConnectorStub implements SmartIdConnector { - - private String sessionIdUsed; - private SessionStatusRequest requestUsed; - private final List responses = new ArrayList<>(); - int responseNumber = 0; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - sessionIdUsed = sessionId; - requestUsed = createSessionStatusRequest(sessionId); - return responses.get(responseNumber++); - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } - - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - return null; - } - - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, - CertificateRequest request) { - return null; - } - - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - return null; - } - - @Override - public SignatureSessionResponse sign(SemanticsIdentifier identifier, - SignatureSessionRequest request) { - return null; - } - - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - return null; - } - - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier identity, - AuthenticationSessionRequest request) { - return null; - } - - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - SessionStatusRequest request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; - } - - @Override - public void setSslContext(SSLContext sslContext) { - - } - } -} diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java deleted file mode 100644 index c93c1352..00000000 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdConnectorSpy.java +++ /dev/null @@ -1,115 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -public class SmartIdConnectorSpy implements SmartIdConnector { - - public SessionStatus sessionStatusToRespond; - public CertificateChoiceResponse certificateChoiceToRespond; - public SignatureSessionResponse signatureSessionResponseToRespond; - public AuthenticationSessionResponse authenticationSessionResponseToRespond; - - public String sessionIdUsed; - public SemanticsIdentifier semanticsIdentifierUsed; - public String documentNumberUsed; - public CertificateRequest certificateRequestUsed; - public SignatureSessionRequest signatureSessionRequestUsed; - public AuthenticationSessionRequest authenticationSessionRequestUsed; - - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - sessionIdUsed = sessionId; - return sessionStatusToRespond; - } - - @Override - public CertificateChoiceResponse getCertificate(String documentNumber, CertificateRequest request) { - documentNumberUsed = documentNumber; - certificateRequestUsed = request; - return certificateChoiceToRespond; - } - - @Override - public CertificateChoiceResponse getCertificate(SemanticsIdentifier identifier, CertificateRequest request) { - semanticsIdentifierUsed = identifier; - certificateRequestUsed = request; - return certificateChoiceToRespond; - } - - @Override - public SignatureSessionResponse sign(String documentNumber, SignatureSessionRequest request) { - documentNumberUsed = documentNumber; - signatureSessionRequestUsed = request; - return signatureSessionResponseToRespond; - } - - @Override - public SignatureSessionResponse sign(SemanticsIdentifier identifier, SignatureSessionRequest request) { - semanticsIdentifierUsed = identifier; - signatureSessionRequestUsed = request; - return signatureSessionResponseToRespond; - } - - @Override - public AuthenticationSessionResponse authenticate(String documentNumber, AuthenticationSessionRequest request) { - documentNumberUsed = documentNumber; - authenticationSessionRequestUsed = request; - return authenticationSessionResponseToRespond; - } - - @Override - public AuthenticationSessionResponse authenticate(SemanticsIdentifier identifier, AuthenticationSessionRequest request) { - semanticsIdentifierUsed = identifier; - authenticationSessionRequestUsed = request; - return authenticationSessionResponseToRespond; - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - - } - - @Override - public void setSslContext(SSLContext sslContext) { - - } -} diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java deleted file mode 100644 index d19e422b..00000000 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestConnectorTest.java +++ /dev/null @@ -1,746 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.containing; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubBadRequestResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubForbiddenResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubUnauthorizedResponse; -import static java.util.Arrays.asList; -import static org.hamcrest.core.StringStartsWith.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; -import org.glassfish.jersey.client.ClientConfig; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.ClientRequestHeaderFilter; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -@WireMockTest(httpPort = 18089) -class SmartIdRestConnectorTest { - - private SmartIdConnector connector; - - @BeforeEach - public void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void getNotExistingSessionStatus() { - assertThrows(SessionNotFoundException.class, () -> { - stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - }); - } - - @Test - void getRunningSessionStatus() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunning.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - } - - @Test - void getRunningSessionStatus_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusRunningWithIgnoredProperties.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - assertNotNull(sessionStatus.getIgnoredProperties()); - assertEquals(2, sessionStatus.getIgnoredProperties().length); - assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); - assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); - } - - @Test - void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); - assertSuccessfulResponse(sessionStatus); - assertNotNull(sessionStatus.getCert()); - MatcherAssert.assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHhjCCBW6gAwIBAgIQDNYLtVwrKURYStrYApYViTANBgkqhkiG9")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_forSuccessfulSigningRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); - assertSuccessfulResponse(sessionStatus); - assertNotNull(sessionStatus.getSignature()); - MatcherAssert.assertThat(sessionStatus.getSignature().getValue(), startsWith("luvjsi1+1iLN9yfDFEh/BE8hXtAKhAIxilv")); - assertEquals("sha256WithRSAEncryption", sessionStatus.getSignature().getAlgorithm()); - } - - @Test - void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusForSuccessfulSigningRequest.json"); - assertSuccessfulResponse(sessionStatus); - - verify(getRequestedFor(urlMatching("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessage.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); - } - - @Test - void getSessionStatus_userHasRefusedRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedConfirmationMessageWithVerificationCodeChoice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); - } - - @Test - void getSessionStatus_userHasRefusedWhenUserRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedDisplayTextAndPin.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); - } - - @Test - void getSessionStatus_userHasRefusedWhenUserRefusedGeneral() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedGeneral.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); - } - - @Test - void getSessionStatus_userHasRefusedWhenUserRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserRefusedVerificationCodeChoice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); - } - - @Test - void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenTimeout.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); - } - - @Test - void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenUserHasSelectedWrongVcCode.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); - } - - @Test - void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("v2/responses/sessionStatusWhenDocumentUnusable.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); - } - - @Test - void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - assertSuccessfulResponse(sessionStatus); - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); - } - - @Test - void getCertificate_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - void getCertificate_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/PASKZ-987654321012", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PASKZ-987654321012"); - - CertificateRequest request = createDummyCertificateRequest(); - CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - void getCertificate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - request.setNonce("zstOt2umlc"); - CertificateChoiceResponse response = connector.getCertificate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - void getCertificate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/certificatechoice/etsi/IDCCZ-1234567890", "v2/requests/certificateChoiceRequestWithNonce.json", "v2/responses/certificateChoiceResponse.json"); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, "CZ", "1234567890"); - CertificateRequest request = createDummyCertificateRequest(); - request.setNonce("zstOt2umlc"); - CertificateChoiceResponse response = connector.getCertificate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("97f5058e-e308-4c83-ac14-7712b0eb9d86", response.getSessionID()); - } - - @Test - void getCertificate_whenDocumentNumberNotFound_shoudThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void getCertificate_semanticsIdentifierNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/certificatechoice/etsi/IDCCZ-1234567890", "v2/requests/certificateChoiceRequest.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("IDCCZ-1234567890"); - - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate(semanticsIdentifier, request); - }); - } - - @Test - void getCertificate_withWrongAuthenticationParams_shuldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void getCertificate_withWrongRequestParams_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void getCertificate_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void getCertificate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", 480); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void getCertificate_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", 580); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - }); - } - - @Test - void sign_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_hasUserAgentHeader() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionResponse response = connector.sign("PNOEE-123456", createDummySignatureSessionRequest()); - assertNotNull(response); - - verify(postRequestedFor(urlMatching("/signature/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void sign_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequestWithNonce.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - request.setNonce("zstOt2umlc"); - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_fallbackTo_displayTextAndPIN.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessageInteraction = Interaction.confirmationMessage("Do you want to transfer 200 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackInteraction = Interaction.displayTextAndPIN("Transfer 200 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessageInteraction, fallbackInteraction)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_withAllowedInteractionsOrder_confirmationMessageAndNoFallback() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_noFallback.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confi = Interaction.confirmationMessage("Do you want to transfer 999 Bison dollars from savings account to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(Collections.singletonList(confi)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_withAllowedInteractionsOrder_verificationCodeChoiceAndFallbackToDisplayTextAndPIN() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_verificationCodeChoice_fallbackTo_displayTextAndPIN.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction verificationCodeChoice = Interaction.verificationCodeChoice("Transfer 444 BSD to Oceanic Airlines?"); - Interaction fallbackToDisplayTextAndPIN = Interaction.displayTextAndPIN("Transfer 444 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(verificationCodeChoice, fallbackToDisplayTextAndPIN)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_withAllowedInteractionsOrder_confirmationMessageAndFallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessage_fallbackTo_verificationCodeChoice.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_withAllowedInteractionsOrder_confirmationMessageAndVerificationCodeChoice_fallbackToVerificationCodeChoice() { - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signingRequest_confirmationMessageAndVerificationCodeChoice_fallbackTo_verificationCodeChoice.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - - Interaction confirmationMessage = Interaction.confirmationMessage("Do you want to transfer 707 Bison dollars from savings account to Oceanic Airlines?"); - Interaction fallbackToVerificationCodeChoice = Interaction.verificationCodeChoice("Transfer 707 BSD to Oceanic Airlines?"); - request.setAllowedInteractionsOrder(asList(confirmationMessage, fallbackToVerificationCodeChoice)); - - SignatureSessionResponse response = connector.sign("PNOEE-123456", request); - assertNotNull(response); - assertEquals("2c52caf4-13b0-41c4-bdc6-aa268403cc00", response.getSessionID()); - } - - @Test - void sign_whenDocumentNumberNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void sign_withWrongAuthenticationParams_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void sign_withWrongRequestParams_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void sign_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void sign_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", 480); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void sign_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", 580); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - }); - } - - @Test - void authenticate_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - void authenticate_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASKZ-987654321012", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, "KZ", "987654321012"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - void authenticate_withNonce_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setNonce("g9rp4kjca3"); - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - void authenticate_withNonce_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PASEE-48308230504", "v2/requests/authenticationSessionRequestWithNonce.json", "v2/responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PAS, SemanticsIdentifier.CountryCode.EE, "48308230504"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setNonce("g9rp4kjca3"); - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - - @Test - void authenticate_withSingleAllowedInteraction_usingSemanticsIdentifier() { - stubRequestWithResponse("/authentication/etsi/PNOLT-48010010101", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOLT-48010010101"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - AuthenticationSessionResponse response = connector.authenticate(semanticsIdentifier, request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - void authenticate_withSingleAllowedInteraction_usingDocumentNumber() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - AuthenticationSessionResponse response = connector.authenticate("PNOEE-123456", request); - assertNotNull(response); - assertEquals("1dcc1600-29a6-4e95-a95c-d69b31febcfb", response.getSessionID()); - } - - @Test - void authenticate_hasUserAgentHeader() { - stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequestWithSingleAllowedInteraction.json", "v2/responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - request.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - connector.authenticate("PNOEE-123456", request); - - verify(postRequestedFor(urlMatching("/authentication/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void authenticate_whenDocumentNumberNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void authenticate_whenSemanticsIdentifierNotFound_shouldThrowException() { - assertThrows(UserAccountNotFoundException.class, () -> { - stubNotFoundResponse("/authentication/etsi/IDCLV-230883-19894", "v2/requests/authenticationSessionRequest.json"); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.IDC, SemanticsIdentifier.CountryCode.LV, "230883-19894"); - - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate(semanticsIdentifier, request); - }); - } - - @Test - void authenticate_withWrongAuthenticationParams_shuldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubUnauthorizedResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void authenticate_withWrongRequestParams_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubBadRequestResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void authenticate_whenRequestForbidden_shouldThrowException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - stubForbiddenResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void authenticate_whenClientSideAPIIsNotSupportedAnymore_shouldThrowException() { - assertThrows(SmartIdClientException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", 480); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void authenticate_whenSystemUnderMaintenance_shouldThrowException() { - assertThrows(ServerMaintenanceException.class, () -> { - stubErrorResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", 580); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - }); - } - - @Test - void verifyCustomRequestHeaderPresent_whenAuthenticating() { - String headerName = "custom-header"; - String headerValue = "Auth"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/authentication/document/PNOEE-123456", "v2/requests/authenticationSessionRequest.json", "v2/responses/authenticationSessionResponse.json"); - AuthenticationSessionRequest request = createDummyAuthenticationSessionRequest(); - connector.authenticate("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/authentication/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void verifyCustomRequestHeaderPresent_whenSigning() { - String headerName = "custom-header"; - String headerValue = "Sign"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/signature/document/PNOEE-123456", "v2/requests/signatureSessionRequest.json", "v2/responses/signatureSessionResponse.json"); - SignatureSessionRequest request = createDummySignatureSessionRequest(); - connector.sign("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/signature/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void verifyCustomRequestHeaderPresent_whenChoosingCertificate() { - String headerName = "custom-header"; - String headerValue = "Cert choice"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - CertificateRequest request = createDummyCertificateRequest(); - connector.getCertificate("PNOEE-123456", request); - - verify(postRequestedFor(urlEqualTo("/certificatechoice/document/PNOEE-123456")) - .withHeader(headerName, equalTo(headerValue))); - } - - @Test - void getCertificate_hasUserAgentHeader() { - connector = new SmartIdRestConnector("http://localhost:18089"); - stubRequestWithResponse("/certificatechoice/document/PNOEE-123456", "v2/requests/certificateChoiceRequest.json", "v2/responses/certificateChoiceResponse.json"); - connector.getCertificate("PNOEE-123456", createDummyCertificateRequest()); - - verify(postRequestedFor(urlMatching("/certificatechoice/document/PNOEE-123456")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void verifyCustomRequestHeaderPresent_whenRequestingSessionStatus() { - String headerName = "custom-header"; - String headerValue = "Session status"; - - Map headers = new HashMap<>(); - headers.put(headerName, headerValue); - connector = new SmartIdRestConnector("http://localhost:18089", getClientConfigWithCustomRequestHeader(headers)); - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "v2/responses/sessionStatusForSuccessfulCertificateRequest.json"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader(headerName, equalTo(headerValue))); - } - - private ClientConfig getClientConfigWithCustomRequestHeader(Map headers) { - var clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); - clientConfig.register(new ClientRequestHeaderFilter(headers)); - return clientConfig; - } - - private void assertSuccessfulResponse(SessionStatus sessionStatus) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertNotNull(sessionStatus.getResult()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - assertEquals("PNOEE-31111111111", sessionStatus.getResult().getDocumentNumber()); - } - - private void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals(endResult, sessionStatus.getResult().getEndResult()); - } - - private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); - return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } - - private CertificateRequest createDummyCertificateRequest() { - var request = new CertificateRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - return request; - } - - private SignatureSessionRequest createDummySignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - request.setHash("0nbgC2fVdLVQFZJdBbmG7oPoElpCYsQMtrY0c0wKYRg="); - request.setHashType("SHA256"); - request.setAllowedInteractionsOrder(asList( - Interaction.confirmationMessage("Authorize transfer of 1 unit from account 113245344343 to account 7677323232?"), - Interaction.displayTextAndPIN("Transfer 1 unit to account 7677323232?")) - ); - return request; - } - - private AuthenticationSessionRequest createDummyAuthenticationSessionRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - request.setCertificateLevel("ADVANCED"); - request.setHash("K74MSLkafRuKZ1Ooucvh2xa4Q3nz+R/hFWIShN96SPHNcem+uQ6mFMe9kkJQqp5EaoZnJeaFpl310TmlzRgNyQ=="); - request.setHashType("SHA512"); - request.setAllowedInteractionsOrder(asList( - Interaction.confirmationMessageAndVerificationCodeChoice("Log in to self-service?"), - Interaction.displayTextAndPIN("Log in?")) - ); - return request; - } - -} diff --git a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java deleted file mode 100644 index ec56b265..00000000 --- a/src/test/java/ee/sk/smartid/v2/rest/SmartIdRestIntegrationTest.java +++ /dev/null @@ -1,285 +0,0 @@ -package ee.sk.smartid.v2.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static java.util.Arrays.asList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.codec.binary.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.v2.rest.dao.AuthenticationSessionResponse; -import ee.sk.smartid.v2.rest.dao.CertificateChoiceResponse; -import ee.sk.smartid.v2.rest.dao.CertificateRequest; -import ee.sk.smartid.v2.rest.dao.Interaction; -import ee.sk.smartid.v2.rest.dao.SessionStatus; -import ee.sk.smartid.v2.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.v2.rest.dao.SignatureSessionResponse; - -@SmartIdDemoIntegrationTest -public class SmartIdRestIntegrationTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; - private static final String DOCUMENT_NUMBER_LT = "PNOLT-30303039914-MOCK-Q"; - private static final String DATA_TO_SIGN = "Hello World!"; - private static final String CERTIFICATE_LEVEL_QUALIFIED = "QUALIFIED"; - - private SmartIdConnector connector; - - @BeforeEach - public void setUp() { - connector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v2/"); - } - - @Test - public void getCertificateAndSignHash() throws Exception { - CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER_LT); - - SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); - assertCertificateChosen(sessionStatus); - - String documentNumber = sessionStatus.getResult().getDocumentNumber(); - SignatureSessionResponse signatureSessionResponse = createRequestAndFetchSignatureSession(documentNumber); - sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); - assertSignatureCreated(sessionStatus); - } - - @Test - public void authenticate_withSemanticsIdentifier() throws Exception { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); - - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - assertAuthenticationResponseCreated(sessionStatus); - } - - @Test - public void authenticate_withDocumentNumber() throws Exception { - AuthenticationSessionRequest request = createAuthenticationSessionRequest(); - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, request); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - - assertAuthenticationResponseCreated(sessionStatus); - } - - @Test - public void authenticate_withDocumentNumber_advancedInteraction() throws Exception { - AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); - authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - authenticationSessionRequest.setHashType("SHA512"); - authenticationSessionRequest.setHash(calculateHashInBase64(DATA_TO_SIGN.getBytes())); - - authenticationSessionRequest.setAllowedInteractionsOrder( - asList(Interaction.confirmationMessage("Do you want to log in to internet banking system of Oceanic Bank?"), - Interaction.displayTextAndPIN("Log into internet banking system?"))); - - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(DOCUMENT_NUMBER, authenticationSessionRequest); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - org.hamcrest.MatcherAssert.assertThat(sessionStatus.getInteractionFlowUsed(), is("confirmationMessage")); - - assertAuthenticationResponseCreated(sessionStatus); - } - - //@Test CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES - public void getIgnoredProperties_withSign_getIgnoredProperties_withAuthenticate_testAccountsIgnoreVcChoice() throws Exception { - CertificateChoiceResponse certificateChoiceResponse = fetchCertificateChoiceSession(DOCUMENT_NUMBER); - - SessionStatus sessionStatus = pollSessionStatus(certificateChoiceResponse.getSessionID(), connector); - assertCertificateChosen(sessionStatus); - - String documentNumber = sessionStatus.getResult().getDocumentNumber(); - - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - - SignatureSessionResponse signatureSessionResponse = fetchSignatureSession(documentNumber, signatureSessionRequest); - sessionStatus = pollSessionStatus(signatureSessionResponse.getSessionID(), connector); - - assertNotNull(sessionStatus.getResult()); - assertThat(sessionStatus.getResult().getEndResult(), is("OK")); - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - - - assertSignatureCreated(sessionStatus); - assertNotNull(sessionStatus.getIgnoredProperties()); - - assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); - assertThat(sessionStatus.getIgnoredProperties().length, equalTo(2)); - - } - - //@Test //CURRENTLY IGNORED AS DEMO DOESN'T RESPOND BACK IGNORED PROPERTIES - public void getIgnoredProperties_withAuthenticate() throws Exception { - AuthenticationSessionRequest authenticationSessionRequest = createAuthenticationSessionRequest(); - - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "030303-10012"); - - - AuthenticationSessionResponse authenticationSessionResponse = connector.authenticate(semanticsIdentifier, authenticationSessionRequest); - - assertNotNull(authenticationSessionResponse); - assertThat(authenticationSessionResponse.getSessionID(), not(emptyOrNullString())); - - SessionStatus sessionStatus = pollSessionStatus(authenticationSessionResponse.getSessionID(), connector); - - assertThat(sessionStatus.getInteractionFlowUsed(), is("displayTextAndPIN")); - - assertAuthenticationResponseCreated(sessionStatus); - assertNotNull(sessionStatus.getIgnoredProperties()); - - assertThat(asList(sessionStatus.getIgnoredProperties()), containsInAnyOrder("testingIgnored", "testingIgnoredTwo")); - } - - private CertificateChoiceResponse fetchCertificateChoiceSession(String documentNumber) { - CertificateRequest request = createCertificateRequest(); - CertificateChoiceResponse certificateChoiceResponse = connector.getCertificate(documentNumber, request); - assertNotNull(certificateChoiceResponse); - assertThat(certificateChoiceResponse.getSessionID(), not(emptyOrNullString())); - return certificateChoiceResponse; - } - - private CertificateRequest createCertificateRequest() { - CertificateRequest request = new CertificateRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - return request; - } - - private SignatureSessionResponse createRequestAndFetchSignatureSession(String documentNumber) { - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - return fetchSignatureSession(documentNumber, signatureSessionRequest); - } - - private SignatureSessionResponse fetchSignatureSession(String documentNumber, SignatureSessionRequest signatureSessionRequest) { - SignatureSessionResponse signatureSessionResponse = connector.sign(documentNumber, signatureSessionRequest); - assertThat(signatureSessionResponse.getSessionID(), not(emptyOrNullString())); - return signatureSessionResponse; - } - - private SignatureSessionRequest createSignatureSessionRequest() { - SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); - signatureSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - signatureSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - signatureSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - signatureSessionRequest.setHashType("SHA512"); - String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); - signatureSessionRequest.setHash(hashInBase64); - signatureSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log in to bank?"))); - return signatureSessionRequest; - } - - public static AuthenticationSessionRequest createAuthenticationSessionRequest() { - AuthenticationSessionRequest authenticationSessionRequest = new AuthenticationSessionRequest(); - authenticationSessionRequest.setRelyingPartyUUID(RELYING_PARTY_UUID); - authenticationSessionRequest.setRelyingPartyName(RELYING_PARTY_NAME); - authenticationSessionRequest.setCertificateLevel(CERTIFICATE_LEVEL_QUALIFIED); - authenticationSessionRequest.setHashType("SHA512"); - String hashInBase64 = calculateHashInBase64(DATA_TO_SIGN.getBytes()); - authenticationSessionRequest.setHash(hashInBase64); - - authenticationSessionRequest.setAllowedInteractionsOrder(Collections.singletonList(Interaction.displayTextAndPIN("Log into internet banking system"))); - - return authenticationSessionRequest; - } - - public static SessionStatus pollSessionStatus(String sessionId, SmartIdConnector connector1) throws InterruptedException { - SessionStatus sessionStatus = null; - while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - sessionStatus = connector1.getSessionStatus(sessionId); - TimeUnit.SECONDS.sleep(1); - } - assertEquals("COMPLETE", sessionStatus.getState()); - return sessionStatus; - } - - private void assertSignatureCreated(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - assertNotNull(sessionStatus.getSignature()); - assertThat(sessionStatus.getSignature().getValue(), not(emptyOrNullString())); - } - - private void assertCertificateChosen(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - String documentNumber = sessionStatus.getResult().getDocumentNumber(); - assertThat(documentNumber, not(emptyOrNullString())); - assertThat(sessionStatus.getCert().getValue(), not(emptyOrNullString())); - } - - public static void assertAuthenticationResponseCreated(SessionStatus sessionStatus) { - assertNotNull(sessionStatus); - - assertThat(sessionStatus.getResult().getEndResult(), not(emptyOrNullString())); - assertThat(sessionStatus.getSignature().getValue(), not(emptyOrNullString())); - assertThat(sessionStatus.getCert().getValue(), not(emptyOrNullString())); - assertThat(sessionStatus.getCert().getCertificateLevel(), not(emptyOrNullString())); - } - - private static String calculateHashInBase64(byte[] dataToSign) { - byte[] digestValue = DigestCalculator.calculateDigest(dataToSign, HashType.SHA512); - return Base64.encodeBase64String(digestValue); - } -} diff --git a/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java b/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java deleted file mode 100644 index f5164dcb..00000000 --- a/src/test/java/ee/sk/smartid/v2/rest/dao/SignatureSessionRequestTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package ee.sk.smartid.v2.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -public class SignatureSessionRequestTest { - - @Test - public void setDisplayText() { - assertThrows(UnsupportedOperationException.class, () -> { - SignatureSessionRequest signatureSessionRequest = new SignatureSessionRequest(); - signatureSessionRequest.setDisplayText("test"); - }); - } -} diff --git a/src/test/resources/demo_server_trusted_ssl_certs.p12 b/src/test/resources/demo_server_trusted_ssl_certs.p12 deleted file mode 100644 index 1217662ba2ec1ed1749eaf4f91bde401e8d4c179..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6202 zcmV-A7{%u>f*3Xe0Ru3C7w-lMDuzgg_YDCD0ic2x-~@sf+%SR{*f4?@)CLJEhDe6@ z4FLxRpn@00FoGAs0s#Opf)~052`Yw2hW8Bt2LUiC1_~;MNQU^uHH0-T!5E(h|NCK0s{cUP=JCLfCYD(hqDGf&IK+0E9Q+AKF^ocKfD){K9%?! zLo&GCa}8q%tY{jmz^C(PI^Ocwr!cxB{VYd6@$M>{jZzxCX8{^Tb=b2o9{ZI)iH{POnndK^t$vj=gReuHs4v4mu= zU%(fhxIPX+w{RM?R^LF4oE@D;HZg+TiN=v@FV`(sws?59_e{60&tjI_^TF?p%kRzG zjxj}CTZ*-ga#*i|$NwgmIQztM7{5vP^}lta`E6Kisd5QOCZh6}rVb$U>j)^y0~RYF zdlb?93`~4h7DXP9-ubjJ4=6N+()d-!cKFSDk#mGDN%vE6PZ8+OzSO9L`DolxjG~{e zL1@|CuR=JKgzz)xB7tFb+B>Ub3 z8`U)AY#s@tm5@>0HTF8uL70FSF>UlUj8Zg>$DZUjKnO~#4>GM!`+~SF%6e5;c+Igp zcHg?lh6}+ru1Y}Pv1(LBa`82d9vTkeSucIQyh>kC7?Av+OfemrBqO!kK7lV#Ao`vK zBYyRHUQg*O8zO=r8i5<|G*D||L_^4FOvE4)<0?P4gMG!9N%XPV!{^j$wX0Xco~uyu zO*7u}Gn5!~!X4|^H{ve7#6e;xOYUTw`c*PKXQgCJP*ibgf@%*seGvB>9A;6u{m1#- zFZg@F2eGopcD#~I`s`&2HPaSk18Y~)BvS%QvFn7JybeqxJKlxx*VPmNh$VhwGvL>y zsj5Ko<{fA;D>-^Ybq)a5lUgXndqr+?EoV4lJ9*aUo&JwL)7K*xvbtVLGrK}4*>;Zo!BlYsEjCV!YE=Q7Ze z!cY}r3E;$ofS4TN048>3XB0=gfm63^<5iUEw97$SyB8PYs)^}x={i@#!Ob}{?Mz+v zkkc^w=4{9Y>@xDBROn{Q#|WKz^2ib+aK~}IZ{hs<+QSX21g@oK z)`C8@Xm2WO`}sy65ua)mzxr0(G^dbD+|@~+P%BPTBF6zF`i)sW4pH0Ar4C~Da6i^j zy*+=ts&Y!C&F(hHpuX6{neTSbT@QR=ptyhD=dtRWlH0(`pDc=i0!~Q$^mXp-9~OE5 z`zpUj$<2TqSn_L`xw3FRaLxW4rni!Jct#0((P@Q^2mCCF@aHu*z4g&A8pAjOo&c~6 zmn-@aS(Q>Sly|Ve6u;2UB)^jobB(lBoJxYqJXfW%tv||E3vkjQ+<#6wYE@MmQS+Jq z)XcaG5oPW5cZlL;%r)7exXpxJxQ9H0L*IcFl@V3F{#}y7?ShPEOjiCL;`&wc@xPDglZLs3vzQ`x-%jDy?qJ?_-DBRDGg}6&#=2pVKm#n40`$s

      h%3a>Rj8&5F|O_J zlWgPgM0?w^2Mg%d7lDxz!lwpak!4U=bHS9Y-CDbW)0=9fD}+8gStcTMHT#9Ga~=4E z<;vjpiAcB1=?m--`9PmEod7weP+msfUtaGQ5WpTRC^?;{6Hb6}GY@&4z@MpbzvgZX zpHSOaZ5W1t-=B%q78 z>^(G|gtUVwMJY!Pa@trG_CspIkTP&zK2enGlv)Gn0L23(>vuc(zjt)4RlOXO4G8|I zk~wq1Scv~8$Hvi(s0tvXiLH6#Hor0C&~t*=G{oKQv9sP$>vkj8;( z@yU=yJe_!m5hoxsKqxd~*YCo&lC9P|aGnO*koI7Ku!gJoENG8G& zk`8tn-x?RZOF23rnIm=U%PG~YGzPkTJFMThN zsq0~hrjQmCF8qwlO;4u&po5Bo(*LCd?JCl&)H*d?41dk$qD~KmHP86GN^zL+uU$lZ z+?2HewBx%@#49wtKi-3Agy_@t$NI0uQ{zE|oVbj<%nz`ib1Iv6Aa1geRPP5ng7-YS zP?7u_EPB(KmDF8^Cc*$3#&$`dC?UEYb5b=HFWf!3$R&_mGFZtTU&jk zlJF$SMTcbtKfFBPT&-%vH;W#}6U|F`AzOObiXLG?yL)$|MvggEA*}FIU;xdWm@!Lr zA}hFFi>eZ$jaP+VsrMS~=sVva{aFR27P!VaH4_;Dwn|%`jIZ<`N}JrGQAA_(1EnF8 zmB3?dfn6^tq?#+Mr5AEh8%NGT0(0zsi#XHc7FTLz1}AbWgl7*h+G*Y@U={29-5Xp6 zAG4cH2!pHR-ndc+g6)2u& z2@5&AHyt|nI%BaTuIt;pGK?E3l|y2hF`!Lb2y67*;jDOZjS~h12%!mxy7Uot4<_tn zj5(ToQcT4mm1Mr_{}Q`ic!BJSjdV9SkUC!6oEx~_GznLOMcY=gSjdTnmwAeL;weY$ z(GsZ%mFeUrH)9Kr|JGy+H424$1b;p?-2_ZwrJ=ZPFXVuGkC%U&J$YUN2L(QMSF{RV zh4hS(l^Ij2F7+LSs4X<4iBxW8>0(v+(JXK#{DzUNMQMOS#5}>2@q}K5!thKY?^~Rs z1y}bwP^t07y0|xxs^;-MH*2?IZu}B)gKtPZ<-KkPnln@~%i5`5#Yw&_z7$z;9A&Eh zZnYx)~NdZ?1Ix>h8+i@ZpX^@^L$p2~2_Fm0zvxkF`)^VUgMnGgB zT4wYcd*_P5SvD}syo5YnDS^(O*~`dj!=8Dh9)XSS{YQ5YQ(Hk;xQ%~(j8}sQHJQNQeX0|w+WS{4 zMKY62r5Be#Dg*f6u9_9!+tr{6dP+OAlQB-xHW?9(3%DUHp&P<8mJs+laiCl#_op04 z!)Rtj=2dPP&awIz*R<4fI5skUt?vPyjyZ8T5p`yn+;&3i@@yj!v#FT+BCekR3+ zEet~m4gC((9Qdbbs{F)MllWnPrUeUMDE0Q5(^G6b$|Rbhw`$M!+c}Qv>+H0qExWip z)*at?jGnmdT66exfMy%ZU_nX7isNBnc=jiSW+E~zv=@>_F@SNfKIkfDJk~^>L1>h= z(tFI}mJ?!pIOv{|V7js0h@K8GIBjK4a|K>9Q3vGxbMbxnpkk4NozL6tT5b3W2b-v* zNjO$hH<2V4JF~7T=03eQ={5(fqhdcSuZD73ZskiKVQ;YXxmID+JlF10OAOT9^g@%ei zB5g<7Sy7yo*%mlpKbQ0nBMFy(k-t&%vF#C5yE_3Pis~A(6Jb;xv;0|pzdaQ?%Q=l!MS9zBf5M$YI^{>ie`xH^v=yjy zQ&a#}y5e2@D`uPP&1-EvE+!PC7Lm^#Hvg*Au=~q%wbjLEF}dn zv6y+_3cSmeLD=mXKDM(}Nx-Gng^WC7z!=@Qt7;x}+gJckn7Qs{r|q?cVjO1ghbQZaX;h z#iaHRR3{i0R94b;=^PhZ!Oa4AX3?X9ynJYmY0Mf3@N&9fu=`(d1>VOe z{^E1ZR));5O6MD`*{!b4wDR*N1%QB^2CU-DZ+dpouyUO7lmB;uA;{nJ7hym|1<@;$ z>&zOCE7hs&jEh~5jVG0}wOoEIfc`DXee*r(k1W?-n7?VE%8goc7q+pjUO(BasNXUl zH{nq(&@IvT&|U^1Om1~O*@NXb$n3)RcvsoG8A#}}SEg`)xECLQO>OD*YdW>Z(vOiT ztyOOdbw4aIV!M`VCkB6z2p2kN?-ZDU^(D(tCt;6B`<~RQ?|12=um+n)Nu*-GFWnEw z%iV;Ad|w{(#z2gVH0qs3YlNFc*KU0KypVn*+SHTG!fo8ozE4dDOquY00S6`O_B6M2 z5??-Rv$CP5i{$qk_fF1061@9ghl@ozM)dSkU%N;MZTbqjSeteY(idSxcZ57!A zpR#`0i5zXv-oZ29@{$iNL>PaCvF7_ddwYPGy=OaW!0L`K(}~%m5Sg<)PAo>UzXCEVF!b8C-3H|2Cl3EL+=E8l7!x?-ERqh8dOdi^LUJ$Qq57j!^W!I#>7h6} zX|6joXShY@c*xcKzgnl^oViXmFsH{FBPzf6bw?5Dl91-Qkc*%&Rv@P%1;ZsNTwu?R zp6o}~?1x8JK5UYAqj!@$x;gG@Lu0H#ox?+3#8U2p0L8pBJsw|tH#Nk|?I)Tx`FwY^ zVqL@LH{sj7DS(ahCOVWI5CJT|A~}hQ(AiHS@~q^y_QYs7lvtc!>Yo%-zwlO^gcqb{ z@NA^q^;b*`o9zLcIul?2U~+Cgnb+ZZjNF0QEim<|hKDyhiJ)mq0rmgF)Gt?iQJeL^ z{L3Dg_}l19mc&C4n8~XRr(0zx=->LsbdW9O;?Kg2b^@U&Wk!HTmqomIKl4Y|MM4ZY_ z^#6`sm#m{rqOq)qoyiXP@arXh)rDJqbrUBlg%n^(v_H$Ij?D%}Sm~BI$AY$N#M<|% zeJ}87j*(?`^c2dWVKTJI=1kAF09@`;3g8d&pa>D1U~l$C`>}PbmEdFAGNrKLv$YAAa*`G9e4lLO?PMo#)s z;q)1{m{vNRF<%gas*piV99%^$;Bz-4?ccU8lLVblR3)-m7eD#g)_9k!KaxZKh!?*d z(at)6TkvfN5F|yC>LRn(*+z=m zR(>kG+BBoyMg)NEiDrXcl(HCR_Cog9Au(zV(dKwmLDsj(UgqP>N>eF1%B+^atq+$L z{o9;-kG^a95B+vpr9!F)j-Ru$UV6jr06~wUKG}rsK*6$s{dH`$AkUh^A^#*gSPS}z zrwm^Rm7M7=YbBdqxEz8M#AF#$SEwmj%=WUI{}?=vnS*`z#V-OvEKd(Zvlhi{Iw-o2WJ_z>8M0pW%-X6BZIOIU!fB2(A_A0`YQN$>ia=GsraMWEae*S{mE<8Zw6exxE$F#yaiu0s{etpoIGYZ~y=R diff --git a/src/test/resources/v3/requests/certificate-choice-session-request.json b/src/test/resources/requests/certificate-choice-session-request.json similarity index 100% rename from src/test/resources/v3/requests/certificate-choice-session-request.json rename to src/test/resources/requests/certificate-choice-session-request.json diff --git a/src/test/resources/v3/requests/dynamic-link-authentication-session-request.json b/src/test/resources/requests/dynamic-link-authentication-session-request.json similarity index 100% rename from src/test/resources/v3/requests/dynamic-link-authentication-session-request.json rename to src/test/resources/requests/dynamic-link-authentication-session-request.json diff --git a/src/test/resources/v3/requests/dynamic-link-signature-request.json b/src/test/resources/requests/dynamic-link-signature-request.json similarity index 100% rename from src/test/resources/v3/requests/dynamic-link-signature-request.json rename to src/test/resources/requests/dynamic-link-signature-request.json diff --git a/src/test/resources/v3/requests/notification-authentication-session-request.json b/src/test/resources/requests/notification-authentication-session-request.json similarity index 100% rename from src/test/resources/v3/requests/notification-authentication-session-request.json rename to src/test/resources/requests/notification-authentication-session-request.json diff --git a/src/test/resources/v3/requests/notification-signature-session-request.json b/src/test/resources/requests/notification-signature-session-request.json similarity index 100% rename from src/test/resources/v3/requests/notification-signature-session-request.json rename to src/test/resources/requests/notification-signature-session-request.json diff --git a/src/test/resources/v3/responses/dynamic-link-authentication-session-response.json b/src/test/resources/responses/dynamic-link-authentication-session-response.json similarity index 100% rename from src/test/resources/v3/responses/dynamic-link-authentication-session-response.json rename to src/test/resources/responses/dynamic-link-authentication-session-response.json diff --git a/src/test/resources/v3/responses/dynamic-link-certificate-choice-session-response.json b/src/test/resources/responses/dynamic-link-certificate-choice-session-response.json similarity index 100% rename from src/test/resources/v3/responses/dynamic-link-certificate-choice-session-response.json rename to src/test/resources/responses/dynamic-link-certificate-choice-session-response.json diff --git a/src/test/resources/v3/responses/dynamic-link-signature-session-response.json b/src/test/resources/responses/dynamic-link-signature-session-response.json similarity index 100% rename from src/test/resources/v3/responses/dynamic-link-signature-session-response.json rename to src/test/resources/responses/dynamic-link-signature-session-response.json diff --git a/src/test/resources/v3/responses/notification-certificate-choice-session-response.json b/src/test/resources/responses/notification-certificate-choice-session-response.json similarity index 100% rename from src/test/resources/v3/responses/notification-certificate-choice-session-response.json rename to src/test/resources/responses/notification-certificate-choice-session-response.json diff --git a/src/test/resources/v3/responses/notification-session-response.json b/src/test/resources/responses/notification-session-response.json similarity index 100% rename from src/test/resources/v3/responses/notification-session-response.json rename to src/test/resources/responses/notification-session-response.json diff --git a/src/test/resources/v3/responses/session-status-document-unusable.json b/src/test/resources/responses/session-status-document-unusable.json similarity index 100% rename from src/test/resources/v3/responses/session-status-document-unusable.json rename to src/test/resources/responses/session-status-document-unusable.json diff --git a/src/test/resources/v3/responses/session-status-running-with-ignored-properties.json b/src/test/resources/responses/session-status-running-with-ignored-properties.json similarity index 100% rename from src/test/resources/v3/responses/session-status-running-with-ignored-properties.json rename to src/test/resources/responses/session-status-running-with-ignored-properties.json diff --git a/src/test/resources/v3/responses/session-status-running.json b/src/test/resources/responses/session-status-running.json similarity index 100% rename from src/test/resources/v3/responses/session-status-running.json rename to src/test/resources/responses/session-status-running.json diff --git a/src/test/resources/v3/responses/session-status-successful-authentication.json b/src/test/resources/responses/session-status-successful-authentication.json similarity index 100% rename from src/test/resources/v3/responses/session-status-successful-authentication.json rename to src/test/resources/responses/session-status-successful-authentication.json diff --git a/src/test/resources/v3/responses/session-status-successful-certificate-choice.json b/src/test/resources/responses/session-status-successful-certificate-choice.json similarity index 100% rename from src/test/resources/v3/responses/session-status-successful-certificate-choice.json rename to src/test/resources/responses/session-status-successful-certificate-choice.json diff --git a/src/test/resources/v3/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json similarity index 100% rename from src/test/resources/v3/responses/session-status-successful-signature.json rename to src/test/resources/responses/session-status-successful-signature.json diff --git a/src/test/resources/v3/responses/session-status-timeout.json b/src/test/resources/responses/session-status-timeout.json similarity index 100% rename from src/test/resources/v3/responses/session-status-timeout.json rename to src/test/resources/responses/session-status-timeout.json diff --git a/src/test/resources/v3/responses/session-status-user-refused-cert-choice.json b/src/test/resources/responses/session-status-user-refused-cert-choice.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused-cert-choice.json rename to src/test/resources/responses/session-status-user-refused-cert-choice.json diff --git a/src/test/resources/v3/responses/session-status-user-refused-confirmation-vc-choice.json b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused-confirmation-vc-choice.json rename to src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json diff --git a/src/test/resources/v3/responses/session-status-user-refused-confirmation.json b/src/test/resources/responses/session-status-user-refused-confirmation.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused-confirmation.json rename to src/test/resources/responses/session-status-user-refused-confirmation.json diff --git a/src/test/resources/v3/responses/session-status-user-refused-display-text-and-pin.json b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused-display-text-and-pin.json rename to src/test/resources/responses/session-status-user-refused-display-text-and-pin.json diff --git a/src/test/resources/v3/responses/session-status-user-refused-vc-choice.json b/src/test/resources/responses/session-status-user-refused-vc-choice.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused-vc-choice.json rename to src/test/resources/responses/session-status-user-refused-vc-choice.json diff --git a/src/test/resources/v3/responses/session-status-user-refused.json b/src/test/resources/responses/session-status-user-refused.json similarity index 100% rename from src/test/resources/v3/responses/session-status-user-refused.json rename to src/test/resources/responses/session-status-user-refused.json diff --git a/src/test/resources/v3/responses/session-status-wrong-vc.json b/src/test/resources/responses/session-status-wrong-vc.json similarity index 100% rename from src/test/resources/v3/responses/session-status-wrong-vc.json rename to src/test/resources/responses/session-status-wrong-vc.json diff --git a/src/test/resources/sid_live_sk_ee.pem b/src/test/resources/sid_live_sk_ee.pem deleted file mode 100644 index 8aabbe2c..00000000 --- a/src/test/resources/sid_live_sk_ee.pem +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGjjCCBXagAwIBAgIQA6feGFsbcuz3yYop3036xzANBgkqhkiG9w0BAQsFADBN -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E -aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTkxMTAxMDAwMDAwWhcN -MjExMTA1MTIwMDAwWjBaMQswCQYDVQQGEwJFRTEQMA4GA1UEBxMHVGFsbGlubjEb -MBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRwwGgYDVQQDExNycC1hcGkuc21h -cnQtaWQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuycMJZaS -laHLAYvqSFLoTZUF61EPrU4SiYmNqpvoAR7A/ywfjsZUyil1xBYwKI9+wZ4fW1Lj -jgzAY5p26ueGQSx/qHSU5D4ISL6dYvV1zvg5KRYtf1PxPFCOIhwzvoj8XnuiJoBt -/wZmekB90giFRaeUmM2hCU9j78AM6hVJxMsvjP9Kpua4Hc4RJJSZwpnjO8nLO1BO -dRf1M6TFqkYqUYtSJ8Y2NTalgo2gcPw+peN74MomRRB7oIRK6jUsUzwMDaJ0GTan -gnLY1VIgdJhN9EIrIkisJMQJYcabh6KV/s1JG+wTpoC8usqFE/r4ILmTU+BeXL38 -yJXHoGhmkyvCBQIDAQABo4IDWzCCA1cwHwYDVR0jBBgwFoAUD4BhHIIxYdUvKOeN -Rji0LOHG2eIwHQYDVR0OBBYEFDfsZsmLfC1FetD3tQu+TR6qdAlgMB4GA1UdEQQX -MBWCE3JwLWFwaS5zbWFydC1pZC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQW -MBQGCCsGAQUFBwMBBggrBgEFBQcDAjBrBgNVHR8EZDBiMC+gLaArhilodHRwOi8v -Y3JsMy5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNybDAvoC2gK4YpaHR0cDov -L2NybDQuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nNi5jcmwwTAYDVR0gBEUwQzA3 -BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu -Y29tL0NQUzAIBgZngQwBAgIwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho -dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl -cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw -DAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFnAHYAu9nfvB+K -cbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFuJnDpmQAABAMARzBFAiBOZX5E -oZTVzSXTZFgxNf16qm8UJz2h3ipNicc3Jk7T5gIhALLh+P1hMSmN+GZ6j2Q0Ithd -0XCzzLyepocD9MoS5lGgAHYAh3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16g -gw8AAAFuJnDp9wAABAMARzBFAiARiorj+Iahj3ht/QurQ8jhKY3G2gSTpLifh6YW -w+I+egIhAIQCtaaIjKXP5a8jJbKSphUVmj0f78wX0F3flqSOqbyBAHUARJRlLrDu -zq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gagAAAFuJnDpAAAABAMARjBEAiBnqbvU -9b50/orscwLl8Ynyggfym7rsnfX4zkbq/Iun0gIgG1ar0X2/vLa7PKlgCWmnzNM1 -fM2ex6zBYjjBHNjN5GAwDQYJKoZIhvcNAQELBQADggEBACko+lWd1cqdlSv2GDU2 -FJC6f3rMLOcUr/H6A6taaThUQ9gJ1W/xtlSAldHkwC/X2J9Zuw3MbKn+jV17SFEg -lWu4iMlOSd5RPM51Dc7DyALAceau/I5rchKrYH3hhspJydZhz1ghgyZ3mdwkQE6t -Yv5v+G4jeHwUXxJ5dFFnRLNCHeTDqpa2zOglA/ORRM83NDt4cKTl3CqXWeeteFyu -ulnrt7w+IuCVhV6zywolQsqI5T77nQ4GfB6Cco3s01JWTaOg+DcPnobjwqk0o0mi -/rBcmf49zy9T5O8CW6sABOqRV7RKIRSPEiv3M9IKJd621F/OfgGYwWDepBIk4ex3 -dgE= ------END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates.jks b/src/test/resources/trusted_certificates.jks deleted file mode 100644 index 34df03abc1ba124aac49207627d67eba673fec6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5684 zcmd6r2|SeD9>-@h)+EZ1Z6ZmSIWvZk>=UwOdo9T}29q(PnUO51Aw#82s<%a1Qe;U{ zWUXvT`;1{?D27|Nege8SfwOhrwV7 z=t520B{@_!$A=l{!&s-q4)GxZf-VdOU+s=Q$%6^F<8**N42Iqb;?UbboB%5wiGU*z z5(av^0z#yVsc!Y5Sohf{kYE;CcnQyn_kI=}E+zm28wsL77>#sAp`{RHGC>(&`5h54 zSyN}MGnK`m1=2WdDwRbGrLjY(?2vFa0RS9-3{p(Zh3xEtWd>r&WUTp@u@pZJjmdy| z3=WHmbz(9(SaVZ?43M6FsxVwyI4p!_!VbYvsQ`JWja%xofnIz+-! zqPC8{j=m=d`qu-*fO-4?{lvuWU7W~dEdp3dS_%l-1UVr4C34BB$n71qoNciDtpsrh z<(ZoU7l0w-5VK*>=OTzW1RQoT^P*jEK}Y4V-kTfqvL#%Jckp@sCgqm`R7yp|Zk1i` zsaHHA@=J9vs?<5bQ$5uz|MU)(ig#flwVdJdo%H^Ndmpc9Xlq|@V0L#&n?5GZE5=`` zO38hXV00DYvx%mo@>%0oettPQv*jF$N{Fz+e#f-x{pwrgE9Po$^C^no6eHbzdho+Y zcCU)uZIgEb2G`GHvi(O}> zY#!{LD_xq}-so++)xR-yPWl;di}B!Og%U=iQQmKbk{vOJmnU6vee}>U*~R176_9Ps zYF^1MX(v95{19==vgX*XfsaXgm(zI)rSXcT{Q*{Mg{+Rzmbo@q zRl@w!_~Wg=TDos4FS#hY-_**Ba1()q!{AwQ2pQ-^%mqr&K~hjbK_`3j+;b$6v~0XY z{nG{F)pcj6mU77T<3NEwKpZ8Fnwwv1&uQqqv0R~Ufa@o4p83#PVL1@z|9dtZ{uzaW z5)}i4qs!R$y zY;sw1Q{Xv00>QKY_QBy{bf#YbK9EMIvhn<6;&X{i_W23}P>+uRA3g?r%uR6tR2GiH z;s8^AjEX1&pqHTwP*C0}1Cc}_h;b8tKsgaR+5F)RsPUIjMPY&BRGdiR=@w$Tg#xNm zZ8#Do3;1N1d~;C*#63Y2Z2L=VXs6Bj>nWiO0TfiaZaRra{PvxvHk%cRr?BX3`~;4v zR0@Sf@q^xrK9e30T9I*ZIIQ&3oSH3T3U#4R*NI9j>$QtmClX{*Sz#Ql-gnqX#a=Yg z26?XfCkWU8bK}Bav(D*Vz`mY~MHX)^n%ss_8usn(A8C{M;nPEM5IOVZ|GSoQ2N^ll+^ z3y74>%J5cp7!yDv82YAefOgIf(6PK4a=eSdOZVbl)Jh^OJHz1UF&+67Rj z@Y)Dltk)TP+SJu)qh|D0p_CJ`$*FK$Afh_DSX<~<7rA?xCC}VjUTb+@8oVa-bQm0r z$U7thPmo#Ny?Yg}E5qWpdN1@1B4C#Mo#N{RI|NG$z_OY2aK1{gu~b@s7GoWRUm}5u zUHqd_EWU~a5vb~r+U>)9-bk8OJ7g}1LuNx4dQ$D|RVpIR<@I^^j!3mfCe4!vB4{B< z?QSebA(4RbwUSJVfrywSqyW|ySm)`mOq~folo0iaqX&r}c1lA2Jln~^!3AqcAOl6r zEFvU5L`d4Si9}sb%q(r)8C}5oPsF1IG@zpk#k257@sO=8wCo)xHkl080|~U?#EwAF z5zyeGV9H2;7z!PGSgNaKjji|_v~;?7K}E1!&-T*!_L1CIS$=(9^?go{rAlRU)$B`k z{m(wg%Y9vNFnb)3l-!inv9+P^GD}oy^BWwm zxBR6jD-KQpad4lnu-PO3i1MI%WJ@f38O~<&neI(3gHL8)v)JR-xlvcyodzyG|cqBkb?*yTa>PuzJ#~66Hv5G$xi?RlmfQ&{#P1pqe{~$j8 zYY5Q-rc(z;4aZX%)BJ~!1i>Fa^SC3q$q@OI5N7bAqX*s_66HU2bTqth->w{C^L({b zv7xJ;j7#H3kpru%T!;OyqxQUHbosV9`}f#iRwq{wQ`h3_g$4!2KE?K8q@XoovBO9L zThpzR9(T%^k{v+ZmbrGtSw)*V5i_&2dZj~$_W42j*grt@jkywPg#5x=0%A z%We2H-r+lQbN(kRAOT&VP2js*BA*5NKu4P}(It3-bp%bY4Z_jKK(J!FHGN`>V2%Z6OFIvh@@dM5BHhr-fNap#G9jADzW zAr!`tWD^OxX5u683viht2IS1F{xVNl zR5ml5MS+YGWoD726~^+TaA*{&7MtV8p@u?U2@n1an*3onixy&JlX?S3t}k|{M%&G8 zO$l@e`G%Y`lo83@4xu6uVaie|LU0{AvpPr$WqBVZ zPke@LA%@nwUHMd6+s{aDlQ-SFFw0P&adM8tfclJ+??Ek6QB@uMK zQ|tzRZ3%v+8vmhFkq7g?07YsFD10aIv(@hNt9tTzi;kZ&yBy!s(tT=$XUtoLCyj$q zj7-;J7>t!$`9g0&LC%`Hr`7x@`Aw0IH$2?A|I7xD|+E%r|XmKxnt4e%p(Z-W04!8ZErs?7?SFG<_S*G6jvL8n* zsnJ$%>eetJ1?oK9m{2)fc4}omQ*=?nZya=c-vFijGrRiEyQg^{<2>(6r_2}=p&!4VLW2OWI zvZ-N!@e?)q%BdLu!84k4nScBiz!w)b{=Zm*|Bp*+>=VLG^Sxg=mMLMid67SOSX2_qN>1TVk?In>ZEB37q=bZ2&U-`ltE_kM5pJ>7HN za|nVEY{DN_d}11(n}$ikpb?rD!iSMPz)}bz>;f{v4j{vG6gUJ%5Hmvi-Z6X9medi) z42DBlXCyEo5MAU9`KSpD8{r|amTClu1l&fHV1aC8Q)dASy)eVVIDi+&^XH1CB&Lu< zbpi|ZYMjNqPii*9k49p1Imu!!wun=>Vv@ffm}hN5T?*W3RPbSJvNplC?lcc7O)qZ( ztk2-+bgCJcsrPGOVGt4{5b%UT>Krh8Dgbl#Y7#Sm#1;vXr96>PLh@r%&4Jle7{o+g zELp-QbGbkU&;OhgjE8VC*c8I_GY}aJL#o^6J|;7Vy$XZmVJjKk>T8t_4lm^WS-nc6 ztN3;CLI?FP=d#x;4{}Vs65`qeg{+kO zLv6zc)-xO*BseKh_2f1QlrUvt#rZ7B-qBsPvm~yfZwZhB|mk?k)-T()G+980iH@8P)!4^PKxG9#%7spGbNj?+` zM=WqB$6_CkgMB!Tbw5uJO1y5um9)58a)^Zr2~K9MIc7Y8L<4l z0EU4JaGnkcfvx81BamPrq^72R>Cm6W28IMe+-_s6oIv{Y9T-NFfDkO0mH{lc`N%@V zktBWL1FZUFg~)xTxyI;8O1?0pOjnn_xTb1rs@lV{b;irU`_S^A!x20DWJRpka{3fX@qT@w~19W9jGoCxd zQ*G{(cqFA%9B2z(CYJu3pLsg-G}`rQyc)yZm)z7Jo<4Ta|76tVSHj8A+`v%J$slEP zbM2GKNW~u0DcpG3zqoWKjmPi-&U?3p`CuYTSk)O(k{!$+sy56y*toPe?k5blay-b` zr#+U~E#}tM!aAysUaRsGdnZ{A62q3T`Zhr#E42kWI+-+`g!rH}KQ$we3?Ymlf(XY875e!63sMr1AWaP)<7avW;^R2>7pJw=s4Kb9h%kCUz zb}Pc~XLfp#ZMSzB4PUSgJ(GSi|8!F{tG!ItQd~ervO~@DaOY;(*|?thDf6*4o556ZrLoivr&?yXU=*4^nlAj_4Xz6f8UO Hc&phzSICAn diff --git a/travis.sh b/travis.sh deleted file mode 100644 index f06afcf6..00000000 --- a/travis.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Fail on first error -set -e - -echo "Is pull request: $TRAVIS_PULL_REQUEST" -echo "Tag: $TRAVIS_TAG" -echo "JDK version: $TRAVIS_JDK_VERSION" - -if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$TRAVIS_JDK_VERSION" == "openjdk17" ]; then - echo "Starting to publish" - ./publish.sh - echo "Finished" -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_JDK_VERSION" == "openjdk17" ]; then - ./mvnw -DnvdApiKey="$NVD_key" org.owasp:dependency-check-maven:check -else - ./mvnw test - ./mvnw spotbugs:check -fi From 21af0f1e9fe9abbdf80f9a339817efed8da5ba58 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 28 May 2025 12:25:59 +0300 Subject: [PATCH 20/57] =?UTF-8?q?Merge=20branch=20'master'=20of=20https://?= =?UTF-8?q?github.com/ragnarhaide/smart-id-java=E2=80=A6=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Merge branch 'master' of https://github.com/ragnarhaide/smart-id-java-client into v3.1 # Conflicts: # .travis.yml # pom.xml # src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java # src/test/java/ee/sk/smartid/EndpointSslVerificationIntegrationTest.java # src/test/java/ee/sk/smartid/v2/integration/ReadmeTest.java # src/test/java/ee/sk/smartid/v2/integration/SmartIdIntegrationTest.java # src/test/resources/demo_server_trusted_ssl_certs.jks # src/test/resources/demo_server_trusted_ssl_certs.p12 # src/test/resources/trusted_certificates.jks # travis.sh * SLIB-90 - updated dependency-check-maven plugin * Refactored AuthenticationResponseValidator, updated test and README * SLIB-90 - removed java 8 and 11 from github actions * SLIB-90 - refactored, updated README * SLIB-90 - updated README --------- Co-authored-by: ragnar.haide --- .github/workflows/check.yaml | 2 +- .github/workflows/publish.yaml | 4 +- .github/workflows/tests.yaml | 2 +- README.md | 4 +- .../AuthenticationResponseValidator.java | 92 +++++++++---------- .../AuthenticationResponseValidatorTest.java | 9 +- .../TEST_EID-NQ_2021E.pem.crt | 22 +++++ ...EST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt | 23 +++++ 8 files changed, 103 insertions(+), 55 deletions(-) create mode 100644 src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt create mode 100644 src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 31eff898..12371835 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup java uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0c830d90..099ff7fb 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup java SDK 8 + - name: Setup java SDK 17 uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '17' distribution: 'temurin' cache: maven - diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e870b229..6e4229f8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java-version: ['8', '11', '17', '21'] + java-version: ['17', '21'] name: Run tests with java SDK ${{ matrix.java-version }} steps: diff --git a/README.md b/README.md index ba4b3081..6ef1e03f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://app.travis-ci.com/SK-EID/smart-id-java-client.svg?branch=master)](https://app.travis-ci.com/SK-EID/smart-id-java-client) +[![Tests](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml/badge.svg)](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml) [![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) [![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) @@ -84,7 +84,7 @@ This library supports Smart-ID API v3.1. * creating digital signature ## Requirements - * Java 17 or later + * Java 17 or 21 ## Getting the library diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index 396d7dd7..8b5704c6 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -173,16 +173,6 @@ private void validateSignature(AuthenticationResponse authenticationResponse, St } } - private static Signature getSignature(AuthenticationResponse authenticationResponse) throws NoSuchAlgorithmException { - String algorithm = authenticationResponse.getAlgorithmName().replace("Encryption", ""); - try { - return Signature.getInstance(algorithm); - } catch (NoSuchAlgorithmException ex) { - logger.error("Invalid signature algorithm was provided: {}", algorithm); - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); - } - } - private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { if (requestedCertificateLevel == null) { return; @@ -195,41 +185,6 @@ private void validateCertificateLevel(AuthenticationResponse authenticationRespo } } - private record CertDnDetails(String country, String organization, String commonName) { - - private static CertDnDetails from(X500Principal principal) { - String country = null; - String organization = null; - String commonName = null; - - LdapName ldapName; - try { - ldapName = new LdapName(principal.getName()); - } catch (InvalidNameException e) { - String errorMessage = "Error getting certificate distinguished name"; - logger.error(errorMessage, e); - throw new SmartIdClientException(errorMessage, e); - } - - for (Rdn rdn : ldapName.getRdns()) { - if ("C".equalsIgnoreCase(rdn.getType())) { - country = rdn.getValue().toString(); - } else if ("O".equalsIgnoreCase(rdn.getType())) { - organization = rdn.getValue().toString(); - } else if ("CN".equalsIgnoreCase(rdn.getType())) { - commonName = rdn.getValue().toString(); - } - } - return new CertDnDetails(country, organization, commonName); - } - - private static boolean equal(CertDnDetails first, CertDnDetails second) { - return Objects.equals(first.country, second.country) && - Objects.equals(first.organization, second.organization) && - Objects.equals(first.commonName, second.commonName); - } - } - private void validateCertificateIsTrusted(X509Certificate responseCertificate) { CertDnDetails issuerDn = CertDnDetails.from(responseCertificate.getIssuerX500Principal()); @@ -281,6 +236,16 @@ private void initializeTrustedCACertificatesFromKeyStore(String truststorePath, } } + private static Signature getSignature(AuthenticationResponse authenticationResponse) throws NoSuchAlgorithmException { + String algorithm = authenticationResponse.getAlgorithmName().replace("Encryption", ""); + try { + return Signature.getInstance(algorithm); + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", algorithm); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } + } + private static void validateCertificateNotExpired(X509Certificate certificate) { try { certificate.checkValidity(); @@ -294,4 +259,39 @@ private static String createSignatureData(AuthenticationResponse authenticationR authenticationResponse.getServerRandom(), randomChallenge); } -} + + private record CertDnDetails(String country, String organization, String commonName) { + + private static CertDnDetails from(X500Principal principal) { + String country = null; + String organization = null; + String commonName = null; + + LdapName ldapName; + try { + ldapName = new LdapName(principal.getName()); + } catch (InvalidNameException e) { + String errorMessage = "Error getting certificate distinguished name"; + logger.error(errorMessage, e); + throw new SmartIdClientException(errorMessage, e); + } + + for (Rdn rdn : ldapName.getRdns()) { + if ("C".equalsIgnoreCase(rdn.getType())) { + country = rdn.getValue().toString(); + } else if ("O".equalsIgnoreCase(rdn.getType())) { + organization = rdn.getValue().toString(); + } else if ("CN".equalsIgnoreCase(rdn.getType())) { + commonName = rdn.getValue().toString(); + } + } + return new CertDnDetails(country, organization, commonName); + } + + private static boolean equal(CertDnDetails first, CertDnDetails second) { + return Objects.equals(first.country, second.country) && + Objects.equals(first.organization, second.organization) && + Objects.equals(first.commonName, second.commonName); + } + } +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 7d5c0028..66e43a9c 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -149,7 +149,7 @@ void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidate } @Test - void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndValidSignature_ok() { + void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndInvalidSignature_throwsException() { var validator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); @@ -157,11 +157,14 @@ void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndValidSignature_ok dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForAuthCert")); + dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureData")); dynamicLinkAuthenticationResponse.setServerRandom("serverRandom"); dynamicLinkAuthenticationResponse.setEndResult("OK"); - assertThrows(SmartIdClientException.class, () -> validator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> validator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + + assertEquals("Signature verification failed", ex.getMessage()); } @Test diff --git a/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt new file mode 100644 index 00000000..ae2033e6 --- /dev/null +++ b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDpTCCAwagAwIBAgIQTiO7d7Wr6Flg+BPaeYgVHDAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzIxMTIzMjI2WhcNMzYwNzIxMTIzMjI2WjByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBn6bE+DVXUwO +8gYWoA6tu2gb4ou3Gk55ge6jYehcxehS5RO3GaknTrc2YrLcq6nwrcBoIrkVlDOd +Bfub4oea3zL7VlA/ADQ8PTYexu+0zxk1TEtsj0KHH9lh8f7FR1awo4IBYzCCAV8w +HwYDVR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFLNZ0LWq +a/mBsLQHo63DzpXv8Y5GMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/ +AgEAMGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2su +ZWUvb2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09U +X0cxXzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2su +ZWUvVEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAw +PTA7BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9y +ZXBvc2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYwAMIGIAkIBsJ6X9zwyHP3b28br +WIsid0vqWxOzPFU4GFTH/AqXW71V9WLNBJHsbuBg2VNi4k7CKUW7MpRqL8UI8QX7 +/X7jFxMCQgF+IPUDMXMsV99sgqo/Y6VkZYqiakayHkvECkJCncUfmpqVYUlcAxeZ +zRlYIOz3F5AvYJTrtMP0TR3yASD1GtYs4A== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt new file mode 100644 index 00000000..5ca038c0 --- /dev/null +++ b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE----- From 2d082ba4181e9816a5c76ac66f8f586fea216eca Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Tue, 17 Jun 2025 06:49:55 +0300 Subject: [PATCH 21/57] =?UTF-8?q?SLIB-93=20-=20change=20auth=20session=20i?= =?UTF-8?q?nitialization=20from=20dynamic-link=20to=20dev=E2=80=A6=20(#110?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-93 - refactored, added tests, improved javadoc-s * SLIB-93 - refactored, fixed tests, updated README * SLIB-93 - interactions refacto, fixed javadocs, README * SLIB-93 - renamed withSignatureAlgorithmParameters method to withHashAlgorithm --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 13 + MIGRATION_GUIDE.md | 18 +- README.md | 153 +++-- src/main/java/ee/sk/smartid/AuthCode.java | 20 +- .../smartid/AuthenticationResponseMapper.java | 2 +- .../AuthenticationResponseValidator.java | 30 +- ...nkAuthenticationSessionRequestBuilder.java | 377 +++++++++++ ...entBuilder.java => DeviceLinkBuilder.java} | 54 +- ...namicLinkType.java => DeviceLinkType.java} | 6 +- ...nkAuthenticationSessionRequestBuilder.java | 338 ---------- ...ertificateChoiceSessionRequestBuilder.java | 8 +- ...micLinkSignatureSessionRequestBuilder.java | 31 +- ...onAuthenticationSessionRequestBuilder.java | 32 +- ...icationSignatureSessionRequestBuilder.java | 3 +- ...allenge.java => RpChallengeGenerator.java} | 14 +- .../ee/sk/smartid/SignatureAlgorithm.java | 4 +- .../java/ee/sk/smartid/SignatureProtocol.java | 2 +- .../java/ee/sk/smartid/SmartIdClient.java | 16 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 32 +- .../sk/smartid/rest/SmartIdRestConnector.java | 48 +- ...=> AcspV2SignatureProtocolParameters.java} | 21 +- .../dao/AuthenticationSessionRequest.java | 45 +- ...action.java => DeviceLinkInteraction.java} | 19 +- ...ow.java => DeviceLinkInteractionFlow.java} | 4 +- ...se.java => DeviceLinkSessionResponse.java} | 14 +- .../ee/sk/smartid/rest/dao/HashAlgorithm.java | 49 ++ .../ee/sk/smartid/rest/dao/Interaction.java | 2 +- .../dao/SignatureAlgorithmParameters.java | 42 ++ .../ee/sk/smartid/util/DeviceLinkUtil.java | 50 ++ .../sk/smartid/{ => util}/SignatureUtil.java | 24 +- src/test/java/ee/sk/smartid/AuthCodeTest.java | 24 +- .../AuthenticationResponseMapperTest.java | 20 +- .../AuthenticationResponseValidatorTest.java | 212 +++--- ...thenticationSessionRequestBuilderTest.java | 626 ++++++++++++++++++ ...erTest.java => DeviceLinkBuilderTest.java} | 68 +- ...thenticationSessionRequestBuilderTest.java | 507 -------------- ...ficateChoiceSessionRequestBuilderTest.java | 18 +- ...inkSignatureSessionRequestBuilderTest.java | 56 +- .../java/ee/sk/smartid/InteractionUtil.java | 50 ++ ...thenticationSessionRequestBuilderTest.java | 26 +- ...ionSignatureSessionRequestBuilderTest.java | 14 +- .../ee/sk/smartid/QrCodeGeneratorTest.java | 4 +- ...est.java => RpChallengeGeneratorTest.java} | 10 +- .../smartid/SignatureResponseMapperTest.java | 4 +- .../java/ee/sk/smartid/SignatureUtilTest.java | 54 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 125 ++-- .../sk/smartid/SmartIdRestServiceStubs.java | 4 +- .../integration/ReadmeIntegrationTest.java | 80 +-- .../SmartIdRestIntegrationTest.java | 63 +- .../rest/SmartIdRestConnectorTest.java | 136 ++-- ...e-link-authentication-session-request.json | 14 + ...son => device-link-signature-request.json} | 2 +- ...c-link-authentication-session-request.json | 16 - ...cation-authentication-session-request.json | 8 +- ...otification-signature-session-request.json | 2 +- ...link-authentication-session-response.json} | 1 + ...sion-status-successful-authentication.json | 2 +- 57 files changed, 1979 insertions(+), 1638 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java rename src/main/java/ee/sk/smartid/{DynamicContentBuilder.java => DeviceLinkBuilder.java} (75%) rename src/main/java/ee/sk/smartid/{DynamicLinkType.java => DeviceLinkType.java} (93%) delete mode 100644 src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java rename src/main/java/ee/sk/smartid/{RandomChallenge.java => RpChallengeGenerator.java} (86%) rename src/main/java/ee/sk/smartid/rest/dao/{AcspV1SignatureProtocolParameters.java => AcspV2SignatureProtocolParameters.java} (70%) rename src/main/java/ee/sk/smartid/rest/dao/{DynamicLinkInteraction.java => DeviceLinkInteraction.java} (73%) rename src/main/java/ee/sk/smartid/rest/dao/{DynamicLinkInteractionFlow.java => DeviceLinkInteractionFlow.java} (93%) rename src/main/java/ee/sk/smartid/rest/dao/{DynamicLinkSessionResponse.java => DeviceLinkSessionResponse.java} (86%) create mode 100644 src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java create mode 100644 src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java rename src/main/java/ee/sk/smartid/{ => util}/SignatureUtil.java (64%) create mode 100644 src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java rename src/test/java/ee/sk/smartid/{DynamicContentBuilderTest.java => DeviceLinkBuilderTest.java} (78%) delete mode 100644 src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java create mode 100644 src/test/java/ee/sk/smartid/InteractionUtil.java rename src/test/java/ee/sk/smartid/{RandomChallengeTest.java => RpChallengeGeneratorTest.java} (86%) create mode 100644 src/test/resources/requests/device-link-authentication-session-request.json rename src/test/resources/requests/{dynamic-link-signature-request.json => device-link-signature-request.json} (92%) delete mode 100644 src/test/resources/requests/dynamic-link-authentication-session-request.json rename src/test/resources/responses/{dynamic-link-authentication-session-response.json => device-link-authentication-session-response.json} (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eeb28db..6b8a2b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.1] - 2025-06-02 + +### Changed + +- Renamed dynamic-link authentication to device-link authentication. +- Updated authentication endpoints to use /device-link/ paths. +- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackURL` field with regex validation. +- Added `deviceLinkBase` to session response. + ## [3.1] - 2025-05-20 ### Changed diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 2309fcaa..6d2da39c 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -11,8 +11,8 @@ For Smart-ID v3 API replace `ee.sk.smartid.v2.SmartIdClient` with `ee.sk.smartid ## Migrating authentication To migrate from Smart-ID v2 to Smart-ID v3 authentication you need to change the following: -`ee.sk.smartid.SmartIdClient` provides methods `createDynamicLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. -It is recommended to start using dynamic-link authentication flows from Smart-ID API v3 as these are more secure. +`ee.sk.smartid.SmartIdClient` provides methods `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. +It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. ### Overview of V2 authentication flow @@ -26,11 +26,11 @@ It is recommended to start using dynamic-link authentication flows from Smart-ID ### Moving to V3 authentication flow -1. Replace generating authentication hash with generating random challenge using `RandomChallenge.generate()` -2. [Create dynamic-link authentication builder and set values](README.md#examples-of-initiating-a-dynamic-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` -3. Replace showing verification code with showing dynamic link or QR-code. Recommended to use dynamic link for same device and QR-code for cross-device authentication. - - [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. -4. Querying session status can be done in parallel while displaying dynamic content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. +1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` +2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` +3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. + - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. +4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. 5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseMapper`, that will validate required fields and will also handler errors. `AuthenticationResponse` will be returned when everything is ok. 6. Finally use `ee.sk.smartid.AuthenticationResponseValidator` to validate the certificate and the signature in the response. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. @@ -55,7 +55,7 @@ In here will be focusing on [signing on same device with prior authentication se 2. Poll for session status with `sessionStatusPoller::fetchFinalSessionState(sessionID)`. 3. If session status state is `COMPLETE` then check response with `CertificateChoiceResponseMapper` for errors and to validate required fields. `CertificateChoiceResponse` will be returned when everything is ok. 4. Replace V2 SignableData with `ee.sk.smartid.SignableData`. In V3 SignableData the code to generate verification code was removed other than should be same as before. NB! If you are using Digidoc4j `DataToSign` make sure hash type in signable data matches digest algorithm in DataToSign. -5. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-dynamic-link-signature-session) `createDynamicLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -6. Replace showing verification code with showing dynamic link or QR-code. [Create dynamic link or QR-code](README.md#generating-qr-code-or-dynamic-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. +5. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +6. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. 7. Poll for session status until its complete. 8. Validate session response with `SignatureSessionResponseMapper` and validate required fields. `SignatureSessionResponse` will be returned when everything is ok. \ No newline at end of file diff --git a/README.md b/README.md index 6ef1e03f..0ad56173 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ This library supports Smart-ID API v3.1. * [Logging](#logging) * [Log request payloads](#log-request-payloads) * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) - * [Dynamic link flows](#dynamic-link-flows) - * [Dynamic link authentication session](#dynamic-link-authentication-session) - * [Examples of authentication session](#examples-of-initiating-a-dynamic-link-authentication-session) + * [Device link flows](#device-link-flows) + * [Device link authentication session](#device-link-authentication-session) + * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) - * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-dynamic-link-authentication-session-with-semantics-identifier) + * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) * [Initiating a dynamic-link authentication session with document number](#initiating-a-dynamic-link-authentication-session-with-document-number) * [Dynamic link certificate choice session](#dynamic-link-certificate-choice-session) * [Examples of initiating a dynamic-link certificate choice session](#examples-of-initiating-a-dynamic-link-certificate-choice-session) @@ -36,7 +36,7 @@ This library supports Smart-ID API v3.1. * [Initiating a dynamic-link signature session using document number](#initiating-a-dynamic-link-signature-session-with-document-number) * [Examples of allowed dynamic-link interactions order](#examples-of-allowed-dynamic-link-interactions-order) * [Additional request properties](#additional-dynamic-link-session-request-properties) - * [Generating QR-code or dynamic link](#generating-qr-code-or-dynamic-link) + * [Generating QR-code or dynamic link](#generating-qr-code-or-device-link) * [Generating dynamic link ](#generating-dynamic-link) * [Dynamic link parameters](#dynamic-link-parameters) * [Overriding default values](#overriding-default-values) @@ -162,38 +162,41 @@ client. setTrustStore(trustStore); ``` -## Dynamic-link flows +## Device-link flows -Dynamic-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. -More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/dynamic_link_flows.html +Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. +More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html -### Dynamic-link authentication session +### Device-link authentication session #### Request parameters * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V1. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V1 signature protocol. - * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of objects defining the allowed interactions in order of preference. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. + * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters.. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. * Each interaction object includes: * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `initialCallbackURL`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. #### Response parameters * `sessionID`: A string that can be used to request the session status result. * `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required base URI used to form device link or QR code. -#### Examples of initiating a dynamic-link authentication session +#### Examples of initiating a device-link authentication session ##### Initiating an anonymous authentication session @@ -202,19 +205,22 @@ RP can learn the user's identity only after the user has authenticated themselve ```java // For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. +String rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DynamicLinkSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") - )) - .initAuthenticationSession(); +DeviceLinkSessionResponse authenticationSessionResponse = client + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withSignatureProtocol(SignatureProtocol.ACSP_V2) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") + )) + .initAuthenticationSession(); String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later @@ -222,15 +228,16 @@ String sessionId = authenticationSessionResponse.getSessionID(); String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); +String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -##### Initiating a dynamic-link authentication session with semantics identifier +##### Initiating a device-link authentication session with semantics identifier More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) @@ -243,17 +250,20 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( "30303039914"); // identifier (according to country and identity type reference) // For security reasons a new random challenge must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); +String rpChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DynamicLinkSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() +DeviceLinkSessionResponse authenticationSessionResponse = client + .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") + .withSignatureProtocol(SignatureProtocol.ACSP_V2) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") )) .initAuthenticationSession(); @@ -263,31 +273,35 @@ String sessionId = authenticationSessionResponse.getSessionID(); String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); +String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +// Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -##### Initiating a dynamic-link authentication session with document number +##### Initiating a device-link authentication session with document number ```java String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); +String rpChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating OK authentication sessions status response -DynamicLinkSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() +DeviceLinkSessionResponse authenticationSessionResponse = client + .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) + .withRpChallenge(rpChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") + .withSignatureProtocol(SignatureProtocol.ACSP_V2) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") )) .initAuthenticationSession(); @@ -297,12 +311,13 @@ String sessionId = authenticationSessionResponse.getSessionID(); String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); +String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse +// Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Dynamic-link certificate choice session @@ -344,7 +359,7 @@ String sessionToken = certificateChoice.getSessionToken(); String sessionSecret = certificateChoice.getSessionSecret(); Instant responseReceivedAt = certificateChoice.getReceivedAt(); ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Dynamic-link signature session @@ -412,7 +427,7 @@ Instant receivedAt = signatureResponse.getReceivedAt(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ##### Initiating a dynamic-link signature session with document number @@ -445,7 +460,7 @@ Instant receivedAt = signatureResponse.getReceivedAt(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-dynamic-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Error Handling @@ -469,22 +484,6 @@ try { ### Additional dynamic-link session request properties -#### Using nonce to override idempotent behaviour - -Authentication is used as an example, nonce can also be used with certificate choice and signature sessions requests by using method `withNonce("randomValue")`. -```java -DynamicLinkSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") - )) - // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned - // set nonce to override idempotent behaviour - .withNonce("randomValue") - .initAuthenticationSession(); -``` #### Using request properties to request the IP address of the user's device For the IP to be returned the service provider (SK) must switch on this option. @@ -494,7 +493,7 @@ Authentication is used for an example, shareMdClientIpAddress can also be used w ```java DynamicLinkSessionResponse authenticationSessionResponse = client - .createDynamicLinkAuthentication() + .createDeviceLinkAuthentication() .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" .withAllowedInteractionsOrder(Collections.singletonList( @@ -539,12 +538,12 @@ builder.withAllowedInteractionsOrder(List.of( )); ``` -### Generating QR-code or dynamic link +### Generating QR-code or device link -Documentation to dynamic link and QR-code requirements +Documentation to device link and QR-code requirements https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/dynamic_link_flows.html#_dynamic_link_and_qr_presentation -#### Generating dynamic link +#### Generating device link Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. @@ -552,12 +551,12 @@ Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app * `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. * `version`: Version of the dynamic link. Default value is `0.1`. -* `dynamicLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. +* `deviceLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. * `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. * `sessionToken`: Token from the session response. * `elapsedSeconds`: Elapsed time from when the session response was received. * `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link -* `authCode`: Auth code is HMAC256 hash value generated from dynamicLinkType, sessionType, calculated elapsed seconds since response was received and session secret. Received at and sessions secret can be found from the session response. +* `authCode`: Auth code is HMAC256 hash value generated from deviceLinkType, sessionType, calculated elapsed seconds since response was received and session secret. Received at and sessions secret can be found from the session response. ```java DynamicLinkSessionResponse sessionResponse; // response from the session initiation query. @@ -667,9 +666,9 @@ The session status response includes various fields depending on whether the ses * `state`: RUNNING or COMPLETE * `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) * `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. -* `signatureProtocol`: Either ACSP_V1 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) +* `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) * `signature`: Contains the following fields based on the signatureProtocol used: - * For `ACSP_V1`: value, serverRandom, signatureAlgorithm, hashAlgorithm + * For `ACSP_V2`: value, serverRandom, signatureAlgorithm, hashAlgorithm * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm * `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). * `ignoredProperties`: Any unsupported or ignored properties from the request. @@ -724,7 +723,7 @@ It's important to validate the session status response to ensure that the return * Validate that endResult is OK if the session was successful. * Check the certificate field to ensure it has the required certificate level and that it is signed by a trusted CA. -* For `ACSP_V1` signature validation, compare the digest of the signature protocol, server random, and random challenge. +* For `ACSP_V2` signature validation, compare the digest of the signature protocol, server random, and random challenge. * For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. #### Example of validating the authentication sessions response: @@ -841,8 +840,8 @@ The session status response may return various error codes indicating the outcom * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V1. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V1 signature protocol. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. * `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. diff --git a/src/main/java/ee/sk/smartid/AuthCode.java b/src/main/java/ee/sk/smartid/AuthCode.java index e1d18c3c..f19bf550 100644 --- a/src/main/java/ee/sk/smartid/AuthCode.java +++ b/src/main/java/ee/sk/smartid/AuthCode.java @@ -37,7 +37,7 @@ import ee.sk.smartid.util.StringUtil; /** - * This class is responsible for creating an authentication code hash for the dynamic link. + * This class is responsible for creating an authentication code hash for the device link. */ public final class AuthCode { @@ -47,17 +47,17 @@ private AuthCode() { } /** - * Creates an authentication code hash for the dynamic link with the given time. + * Creates an authentication code hash for the device link with the given time. * - * @param dynamicLinkType the type of the dynamic link @{@link DynamicLinkType} + * @param deviceLinkType the type of the device link @{@link DeviceLinkType} * @param sessionType the type of the session @{@link SessionType} * @param elapsedSeconds the time from session creation response was received * @param sessionSecret the session secret in Base64 format * @return the authentication code in Base64 URL safe format */ - public static String createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, long elapsedSeconds, String sessionSecret) { - validateHashingInputs(dynamicLinkType, sessionType); - String payload = createPayload(dynamicLinkType, sessionType, elapsedSeconds); + public static String createHash(DeviceLinkType deviceLinkType, SessionType sessionType, long elapsedSeconds, String sessionSecret) { + validateHashingInputs(deviceLinkType, sessionType); + String payload = createPayload(deviceLinkType, sessionType, elapsedSeconds); return hashThePayload(payload, sessionSecret); } @@ -83,8 +83,8 @@ public static String hashThePayload(String payload, String sessionSecret) { return Base64.getUrlEncoder().withoutPadding().encodeToString(result); } - private static void validateHashingInputs(DynamicLinkType dynamicLinkType, SessionType sessionType) { - if (dynamicLinkType == null) { + private static void validateHashingInputs(DeviceLinkType deviceLinkType, SessionType sessionType) { + if (deviceLinkType == null) { throw new SmartIdClientException("Dynamic link type must be set"); } if (sessionType == null) { @@ -101,7 +101,7 @@ private static void validatePayloadInputs(String payload, String sessionSecret) } } - private static String createPayload(DynamicLinkType dynamicLinkType, SessionType sessionType, long elapsedSeconds) { - return String.format(PAYLOAD_FORMAT, dynamicLinkType.getValue(), sessionType.getValue(), elapsedSeconds); + private static String createPayload(DeviceLinkType deviceLinkType, SessionType sessionType, long elapsedSeconds) { + return String.format(PAYLOAD_FORMAT, deviceLinkType.getValue(), sessionType.getValue(), elapsedSeconds); } } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index 49dae49d..a2c13b6f 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -114,7 +114,7 @@ private static void validateSignatureProtocol(SessionStatus sessionStatus) { throw new UnprocessableSmartIdResponseException("Signature protocol parameter is missing in session status"); } - if (!SignatureProtocol.ACSP_V1.name().equals(sessionStatus.getSignatureProtocol())) { + if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { logger.error("Invalid signature protocol in sessions status: {}", sessionStatus.getSignatureProtocol()); throw new UnprocessableSmartIdResponseException("Invalid signature protocol in sessions status"); } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index 8b5704c6..7b5b4d9f 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -112,8 +112,8 @@ public void addTrustedCACertificate(X509Certificate certificate) { * @param authenticationResponse Smart-ID authentication response * @return authentication identity */ - public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, String randomChallenge) { - return toAuthenticationIdentity(authenticationResponse, AuthenticationCertificateLevel.QUALIFIED, randomChallenge); + public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, String rpChallenge) { + return toAuthenticationIdentity(authenticationResponse, AuthenticationCertificateLevel.QUALIFIED, rpChallenge); } /** @@ -121,24 +121,24 @@ public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse au * * @param authenticationResponse Smart-ID authentication response * @param requestedCertificateLevel Certificate level used in the authentication session request - * @param randomChallenge Generate string used in the authentication session request + * @param rpChallenge Generate string used in the authentication session request * @return authentication identity */ public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel, - String randomChallenge) { - validateInputs(authenticationResponse, randomChallenge); + String rpChallenge) { + validateInputs(authenticationResponse, rpChallenge); validateCertificate(authenticationResponse, requestedCertificateLevel); - validateSignature(authenticationResponse, randomChallenge); + validateSignature(authenticationResponse, rpChallenge); return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); } - private void validateInputs(AuthenticationResponse authenticationResponse, String randomChallenge) { + private void validateInputs(AuthenticationResponse authenticationResponse, String rpChallenge) { if (authenticationResponse == null) { - throw new SmartIdClientException("Dynamic link authentication response is not provided"); + throw new SmartIdClientException("Device link authentication response is not provided"); } - if (StringUtil.isEmpty(randomChallenge)) { - throw new SmartIdClientException("Random challenge is not provided"); + if (StringUtil.isEmpty(rpChallenge)) { + throw new SmartIdClientException("RP challenge is not provided"); } } @@ -151,7 +151,7 @@ private void validateCertificate(AuthenticationResponse authenticationResponse, validateCertificateLevel(authenticationResponse, requestedCertificateLevel); } - private void validateSignature(AuthenticationResponse authenticationResponse, String randomChallenge) { + private void validateSignature(AuthenticationResponse authenticationResponse, String rpChallenge) { if (StringUtil.isEmpty(authenticationResponse.getAlgorithmName())) { throw new SmartIdClientException("Algorithm name is not provided"); } @@ -161,7 +161,7 @@ private void validateSignature(AuthenticationResponse authenticationResponse, St try { Signature signature = getSignature(authenticationResponse); signature.initVerify(authenticationResponse.getCertificate().getPublicKey()); - String data = createSignatureData(authenticationResponse, randomChallenge); + String data = createSignatureData(authenticationResponse, rpChallenge); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signedHash = authenticationResponse.getSignatureValue(); if (!signature.verify(signedHash)) { @@ -254,10 +254,10 @@ private static void validateCertificateNotExpired(X509Certificate certificate) { } } - private static String createSignatureData(AuthenticationResponse authenticationResponse, String randomChallenge) { - return String.format("%s;%s;%s", SignatureProtocol.ACSP_V1.name(), + private static String createSignatureData(AuthenticationResponse authenticationResponse, String rpChallenge) { + return String.format("%s;%s;%s", SignatureProtocol.ACSP_V2.name(), authenticationResponse.getServerRandom(), - randomChallenge); + rpChallenge); } private record CertDnDetails(String country, String organization, String commonName) { diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java new file mode 100644 index 00000000..b0c162c2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -0,0 +1,377 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Class for building a device link authentication session request + */ +public class DeviceLinkAuthenticationSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(DeviceLinkAuthenticationSessionRequestBuilder.class); + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; + private String rpChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + private String initialCallbackURL; + + /** + * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DeviceLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + *

      + * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the RP challenge. + *

      + * RP challenge is a randomly generated string that must be Base64 encoded and + * should be regenerated for every new authentication session request. + *

      + * You can use {@link ee.sk.smartid.RpChallengeGenerator} to generate a suitable RP challenge. + * + * @param rpChallenge RP challenge in Base64 encoded format + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the hash algorithm to be used for signature creation. + * By default, SHA3-512 is used. + * + * @param hashAlgorithm the hash algorithm to use + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the allowed interactions order + * + * @param interactions the allowed interactions order + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

      + * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

      + * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the initial callback URL. + *

      + * This URL is used to redirect the user after the authentication session is started. + *

      + * The callback URL should be set when using same device flows (like Web2App or App2App). + * + * @param initialCallbackURL the initial callback URL + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackURL(String initialCallbackURL) { + this.initialCallbackURL = initialCallbackURL; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

      + * There are 3 supported ways to start authentication session: + *

        + *
      • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
      • + *
      • with document number by using {@link #withDocumentNumber(String)}
      • + *
      • anonymously if semantics identifier and document number are not provided
      • + *
      + * + * @return init session response + * @throws SmartIdClientException if request parameters are invalid + * @throws UnprocessableSmartIdResponseException if the response is missing required fields + */ + public DeviceLinkSessionResponse initAuthenticationSession() { + validateRequestParameters(); + AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(deviceLinkAuthenticationSessionResponse); + return deviceLinkAuthenticationSessionResponse; + } + + private DeviceLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + logger.error("Both semanticsIdentifier and documentNumber are set – only one can be used"); + throw new SmartIdClientException("Only one of semanticsIdentifier or documentNumber may be set"); + } + if (semanticsIdentifier != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, documentNumber); + } else { + return connector.initAnonymousDeviceLinkAuthentication(authenticationRequest); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + validateSignatureParameters(); + validateInteractions(); + validateInitialCallbackURL(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(rpChallenge)) { + logger.error("Parameter rpChallenge must be set"); + throw new SmartIdClientException("Parameter rpChallenge must be set"); + } + try { + Base64.getDecoder().decode(rpChallenge); + } catch (IllegalArgumentException e) { + logger.error("Parameter rpChallenge is not a valid Base64 encoded string"); + throw new SmartIdClientException("Parameter rpChallenge is not a valid Base64 encoded string"); + } + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + logger.error("Encoded rpChallenge must be between 44 and 88 characters"); + throw new SmartIdClientException("Encoded rpChallenge must be between 44 and 88 characters"); + } + if (signatureAlgorithm == null) { + logger.error("Parameter signatureAlgorithm must be set"); + throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); + } + if (hashAlgorithm == null) { + logger.error("Parameter hashAlgorithm must be set"); + throw new SmartIdClientException("Parameter hashAlgorithm must be set"); + } + } + + private void validateInteractions() { + if (interactions == null || interactions.isEmpty()) { + logger.error("Parameter interactions must be set"); + throw new SmartIdClientException("Parameter interactions must be set"); + } + validateNoDuplicateInteractions(); + interactions.forEach(DeviceLinkInteraction::validate); + } + + private void validateInitialCallbackURL() { + if (!StringUtil.isEmpty(initialCallbackURL) && !initialCallbackURL.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdClientException("initialCallbackURL must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private AuthenticationSessionRequest createAuthenticationRequest() { + var request = new AuthenticationSessionRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + + if (certificateLevel != null) { + request.setCertificateLevel(certificateLevel.name()); + } + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); + signatureProtocolParameters.setRpChallenge(rpChallenge); + signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); + + var signatureAlgorithmParameters = new SignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(this.hashAlgorithm); + signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setInteractions(DeviceLinkUtil.encodeToBase64(interactions)); + + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(); + requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + request.setRequestProperties(requestProperties); + } + request.setCapabilities(capabilities); + request.setInitialCallbackURL(initialCallbackURL); + return request; + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionToken())) { + logger.error("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionSecret())) { + logger.error("Session secret is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); + } + if (deviceLinkAuthenticationSessionResponse.getDeviceLinkBase() == null || deviceLinkAuthenticationSessionResponse.getDeviceLinkBase().toString().isBlank()) { + logger.error("deviceLinkBase is missing or empty in the response"); + throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing or empty in the response"); + } + } + + private void validateNoDuplicateInteractions() { + if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + logger.error("Duplicate values found in interactions"); + throw new SmartIdClientException("Duplicate values in interactions are not allowed"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DynamicContentBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java similarity index 75% rename from src/main/java/ee/sk/smartid/DynamicContentBuilder.java rename to src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index f9e12ed3..5693bc1e 100644 --- a/src/main/java/ee/sk/smartid/DynamicContentBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -33,17 +33,17 @@ import jakarta.ws.rs.core.UriBuilder; /** - * Builds dynamic content. Can be used to generate dynamic link or QR code. + * Builds device link or QR-code. */ -public class DynamicContentBuilder { +public class DeviceLinkBuilder { - private static final String DEFAULT_BASE_URL = "https://smart-id.com/dynamic-link/"; + private static final String DEFAULT_BASE_URL = "https://smart-id.com/device-link/"; private static final String DEFAULT_VERSION = "0.1"; private static final String DEFAULT_USER_LANGUAGE = "eng"; private String baseUrl = DEFAULT_BASE_URL; private String version = DEFAULT_VERSION; - private DynamicLinkType dynamicLinkType; + private DeviceLinkType deviceLinkType; private SessionType sessionType; private String sessionToken; private Long elapsedSeconds; @@ -53,47 +53,47 @@ public class DynamicContentBuilder { /** * Sets the URL *

      - * Defaults to https://smart-id.com/dynamic-link + * Defaults to https://smart-id.com/device-link * * @param baseUrl the URL that will direct to SMART-ID application * @return this builder */ - public DynamicContentBuilder withBaseUrl(String baseUrl) { + public DeviceLinkBuilder withBaseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } /** - * Sets the version of the dynamic link. + * Sets the version of the device link. *

      * Defaults to 0.1 * * @param version the version of * @return this builder */ - public DynamicContentBuilder withVersion(String version) { + public DeviceLinkBuilder withVersion(String version) { this.version = version; return this; } /** - * Sets the type of the dynamic link. Use {@link DynamicLinkType} to set the type. + * Sets the type of the device link. Use {@link DeviceLinkType} to set the type. * - * @param dynamicLinkType the type of the dynamic link the builder is creating + * @param deviceLinkType the type of the device link the builder is creating * @return this builder */ - public DynamicContentBuilder withDynamicLinkType(DynamicLinkType dynamicLinkType) { - this.dynamicLinkType = dynamicLinkType; + public DeviceLinkBuilder withDeviceLinkType(DeviceLinkType deviceLinkType) { + this.deviceLinkType = deviceLinkType; return this; } /** * Sets the type of the session. Use {@link SessionType} to set the type. * - * @param sessionType the type of the session the dynamic link is created for + * @param sessionType the type of the session the device link is created for * @return this builder */ - public DynamicContentBuilder withSessionType(SessionType sessionType) { + public DeviceLinkBuilder withSessionType(SessionType sessionType) { this.sessionType = sessionType; return this; } @@ -104,7 +104,7 @@ public DynamicContentBuilder withSessionType(SessionType sessionType) { * @param sessionToken the session token that was received from the Smart-ID server * @return this builder */ - public DynamicContentBuilder withSessionToken(String sessionToken) { + public DeviceLinkBuilder withSessionToken(String sessionToken) { this.sessionToken = sessionToken; return this; } @@ -115,7 +115,7 @@ public DynamicContentBuilder withSessionToken(String sessionToken) { * @param elapsedSeconds the time passed since the session response was received in seconds * @return this builder */ - public DynamicContentBuilder withElapsedSeconds(Long elapsedSeconds) { + public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { this.elapsedSeconds = elapsedSeconds; return this; } @@ -128,35 +128,35 @@ public DynamicContentBuilder withElapsedSeconds(Long elapsedSeconds) { * @param userLanguage the language of the user * @return this builder */ - public DynamicContentBuilder withUserLanguage(String userLanguage) { + public DeviceLinkBuilder withUserLanguage(String userLanguage) { this.userLanguage = userLanguage; return this; } /** - * Sets the auth code that will be used in the dynamic link. + * Sets the auth code that will be used in the device link. * - * @param authCode the auth code in the dynamic link + * @param authCode the auth code in the device link * @return this builder */ - public DynamicContentBuilder withAuthCode(String authCode) { + public DeviceLinkBuilder withAuthCode(String authCode) { this.authCode = authCode; return this; } /** - * Creates a URI that can be used as dynamic link or content for QR-code. + * Creates a URI that can be used as device link or content for QR-code. *

      * To get a QR code image, use {@link #createQrCodeDataUri()} method. * - * @return URI that can be used as dynamic link or content for QR-code + * @return URI that can be used as device link or content for QR-code */ public URI createUri() { validateInputParameters(); return UriBuilder.fromUri(baseUrl) .queryParam("version", version) .queryParam("sessionToken", sessionToken) - .queryParam("dynamicLinkType", dynamicLinkType.getValue()) + .queryParam("dynamicLinkType", deviceLinkType.getValue()) .queryParam("sessionType", sessionType.getValue()) .queryParam("elapsedSeconds", elapsedSeconds) .queryParam("lang", userLanguage) @@ -167,13 +167,13 @@ public URI createUri() { /** * Creates a QR code image as a Base64 encoded string. *

      - * The dynamic link type must be QR_CODE to create a QR code image. + * The device link type must be QR_CODE to create a QR code image. * * @return QR code image as a Base64 encoded string */ public String createQrCodeDataUri() { - if (dynamicLinkType != DynamicLinkType.QR_CODE) { - throw new SmartIdClientException("Dynamic link type must be QR_CODE"); + if (deviceLinkType != DeviceLinkType.QR_CODE) { + throw new SmartIdClientException("Device link type must be QR_CODE"); } return QrCodeGenerator.generateDataUri(createUri().toString()); } @@ -185,7 +185,7 @@ private void validateInputParameters() { if (StringUtil.isEmpty(version)) { throw new SmartIdClientException("Parameter version must be set"); } - if (dynamicLinkType == null) { + if (deviceLinkType == null) { throw new SmartIdClientException("Parameter dynamicLinkType must be set"); } if (sessionType == null) { diff --git a/src/main/java/ee/sk/smartid/DynamicLinkType.java b/src/main/java/ee/sk/smartid/DeviceLinkType.java similarity index 93% rename from src/main/java/ee/sk/smartid/DynamicLinkType.java rename to src/main/java/ee/sk/smartid/DeviceLinkType.java index 906f6d72..5cbf2262 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkType.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkType.java @@ -27,9 +27,9 @@ */ /** - * Enum for dynamic link types + * Enum for device link types */ -public enum DynamicLinkType { +public enum DeviceLinkType { QR_CODE("QR"), WEB_2_APP("Web2App"), @@ -37,7 +37,7 @@ public enum DynamicLinkType { private final String value; - DynamicLinkType(String value) { + DeviceLinkType(String value) { this.value = value; } diff --git a/src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java deleted file mode 100644 index c3f502bf..00000000 --- a/src/main/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilder.java +++ /dev/null @@ -1,338 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; - -/** - * Class for building a dynamic link authentication session request - */ -public class DynamicLinkAuthenticationSessionRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(DynamicLinkAuthenticationSessionRequestBuilder.class); - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; - private String randomChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; - private String nonce; - private List allowedInteractionsOrder; - private Boolean shareMdClientIpAddress; - private Set capabilities; - private SemanticsIdentifier semanticsIdentifier; - private String documentNumber; - - /** - * Constructs a new DynamicLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public DynamicLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartUUID the relying party UUID - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { - this.relyingPartyUUID = relyingPartUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - *

      - * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the random challenge - *

      - * The provided random challenge must be a Base64 encoded string - * - * @param randomChallenge the signature protocol parameters - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withRandomChallenge(String randomChallenge) { - this.randomChallenge = randomChallenge; - return this; - } - - /** - * Sets the signature algorithm - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the nonce - * - * @param nonce the nonce - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the allowed interactions order - * - * @param allowedInteractionsOrder the allowed interactions order - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); - return this; - } - - /** - * Sets the semantics identifier - *

      - * Setting this value will make the authentication session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the document number - *

      - * Setting this value will make the authentication session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public DynamicLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sends the authentication request and get the init session response - *

      - * There are 3 supported ways to start authentication session: - *

        - *
      • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
      • - *
      • with document number by using {@link #withDocumentNumber(String)}
      • - *
      • anonymously if semantics identifier and document number are not provided
      • - *
      - * - * @return init session response - */ - public DynamicLinkSessionResponse initAuthenticationSession() { - validateRequestParameters(); - AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - DynamicLinkSessionResponse dynamicLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); - validateResponseParameters(dynamicLinkAuthenticationSessionResponse); - return dynamicLinkAuthenticationSessionResponse; - } - - private DynamicLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { - if (semanticsIdentifier != null) { - return connector.initDynamicLinkAuthentication(authenticationRequest, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initDynamicLinkAuthentication(authenticationRequest, documentNumber); - } else { - return connector.initAnonymousDynamicLinkAuthentication(authenticationRequest); - } - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); - } - validateSignatureParameters(); - validateNonce(); - validateAllowedInteractionOrder(); - } - - private void validateSignatureParameters() { - if (StringUtil.isEmpty(randomChallenge)) { - logger.error("Parameter randomChallenge must be set"); - throw new SmartIdClientException("Parameter randomChallenge must be set"); - } - byte[] challenge = getDecodedRandomChallenge(); - if (challenge.length < 32 || challenge.length > 64) { - logger.error("Size of parameter randomChallenge must be between 32 and 64 bytes"); - throw new SmartIdClientException("Size of parameter randomChallenge must be between 32 and 64 bytes"); - } - if (signatureAlgorithm == null) { - logger.error("Parameter signatureAlgorithm must be set"); - throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); - } - } - - private byte[] getDecodedRandomChallenge() { - Base64.Decoder decoder = Base64.getDecoder(); - try { - return decoder.decode(randomChallenge); - } catch (IllegalArgumentException e) { - logger.error("Parameter randomChallenge is not a valid Base64 encoded string"); - throw new SmartIdClientException("Parameter randomChallenge is not a valid Base64 encoded string"); - } - } - - private void validateNonce() { - if (nonce == null) { - return; - } - if (nonce.isEmpty()) { - logger.error("Parameter nonce value has to be at least 1 character long"); - throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); - } - if (nonce.length() > 30) { - logger.error("Nonce cannot be longer that 30 chars"); - throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); - } - } - - private void validateAllowedInteractionOrder() { - if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { - logger.error("Parameter allowedInteractionsOrder must be set"); - throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); - } - allowedInteractionsOrder.forEach(DynamicLinkInteraction::validate); - } - - private AuthenticationSessionRequest createAuthenticationRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); - signatureProtocolParameters.setRandomChallenge(randomChallenge); - signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setNonce(nonce); - request.setAllowedInteractionsOrder(allowedInteractionsOrder); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - request.setCapabilities(capabilities); - return request; - } - - private void validateResponseParameters(DynamicLinkSessionResponse dynamicLinkAuthenticationSessionResponse) { - if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new SmartIdClientException("Session ID is missing from the response"); - } - - if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionToken())) { - logger.error("Session token is missing from the response"); - throw new SmartIdClientException("Session token is missing from the response"); - } - - if (StringUtil.isEmpty(dynamicLinkAuthenticationSessionResponse.getSessionSecret())) { - logger.error("Session secret is missing from the response"); - throw new SmartIdClientException("Session secret is missing from the response"); - } - } -} diff --git a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 2900626e..921ef76b 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -37,7 +37,7 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; public class DynamicLinkCertificateChoiceSessionRequestBuilder { @@ -133,13 +133,13 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddr * which can be used by the Relying Party to manage and verify the session independently. *

      * - * @return DynamicLinkCertificateChoiceSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. * @throws SmartIdClientException if the response is invalid or missing necessary session data. */ - public DynamicLinkSessionResponse initCertificateChoice() { + public DeviceLinkSessionResponse initCertificateChoice() { validateParameters(); CertificateChoiceSessionRequest request = createCertificateRequest(); - DynamicLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); + DeviceLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); if (response == null || response.getSessionID() == null) { throw new UnprocessableSmartIdResponseException("Dynamic link certificate choice session failed: invalid response received."); diff --git a/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java index fb8f9fca..4da6bfca 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java @@ -35,10 +35,11 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; @@ -57,7 +58,7 @@ public class DynamicLinkSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private List allowedInteractionsOrder; + private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private SignableData signableData; @@ -156,7 +157,7 @@ public DynamicLinkSignatureSessionRequestBuilder withCapabilities(String... capa * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { + public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { this.allowedInteractionsOrder = allowedInteractionsOrder; return this; } @@ -234,18 +235,18 @@ public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boole *

    • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
    • *
    * - * @return a {@link DynamicLinkSessionResponse} containing session details such as + * @return a {@link DeviceLinkSessionResponse} containing session details such as * session ID, session token, and session secret. */ - public DynamicLinkSessionResponse initSignatureSession() { + public DeviceLinkSessionResponse initSignatureSession() { validateParameters(); SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - DynamicLinkSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + DeviceLinkSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); validateResponseParameters(dynamicLinkSignatureSessionResponse); return dynamicLinkSignatureSessionResponse; } - private DynamicLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { + private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { if (documentNumber != null) { return connector.initDynamicLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { @@ -268,7 +269,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { if (signableHash != null || signableData != null) { signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); } - signatureProtocolParameters.setSignatureAlgorithm(SignatureUtil.getSignatureAlgorithm(signatureAlgorithm, signableHash, signableData)); + signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setNonce(nonce); request.setAllowedInteractionsOrder(allowedInteractionsOrder); @@ -306,20 +307,24 @@ private void validateAllowedInteractions() { allowedInteractionsOrder.forEach(Interaction::validate); } - private void validateResponseParameters(DynamicLinkSessionResponse dynamicLinkSignatureSessionResponse) { - if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionID())) { + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionID())) { logger.error("Session ID is missing from the response"); throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); } - if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionToken())) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionToken())) { logger.error("Session token is missing from the response"); throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); } - if (StringUtil.isEmpty(dynamicLinkSignatureSessionResponse.getSessionSecret())) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionSecret())) { logger.error("Session secret is missing from the response"); throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); } + if (deviceLinkSignatureSessionResponse.getDeviceLinkBase() == null) { + throw new SmartIdClientException("deviceLinkBase is missing from the response"); + } + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index 612fdac5..aae2599c 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -26,6 +26,7 @@ * #L% */ +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Set; @@ -33,12 +34,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; @@ -59,9 +63,9 @@ public class NotificationAuthenticationSessionRequestBuilder { private String relyingPartyName; private AuthenticationCertificateLevel certificateLevel; private String randomChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.SHA512WITHRSA; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; private String nonce; - private List allowedInteractionsOrder; + private List interactions; private Boolean shareMdClientIpAddress; private Set capabilities; private SemanticsIdentifier semanticsIdentifier; @@ -151,7 +155,7 @@ public NotificationAuthenticationSessionRequestBuilder withNonce(String nonce) { * @return this builder */ public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; + this.interactions = allowedInteractionsOrder; return this; } @@ -287,11 +291,11 @@ private void validateNonce() { } private void validateAllowedInteractionOrder() { - if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + if (interactions == null || interactions.isEmpty()) { logger.error("Parameter allowedInteractionsOrder must be set"); throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); } - allowedInteractionsOrder.forEach(Interaction::validate); + interactions.forEach(Interaction::validate); } private AuthenticationSessionRequest createAuthenticationRequest() { @@ -303,12 +307,11 @@ private AuthenticationSessionRequest createAuthenticationRequest() { request.setCertificateLevel(certificateLevel.name()); } - var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); - signatureProtocolParameters.setRandomChallenge(randomChallenge); + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); + signatureProtocolParameters.setRpChallenge(randomChallenge); signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setNonce(nonce); - request.setAllowedInteractionsOrder(allowedInteractionsOrder); + request.setInteractions(encodeInteractionsToBase64(interactions)); if (this.shareMdClientIpAddress != null) { var requestProperties = new RequestProperties(); @@ -347,4 +350,13 @@ private void validateResponseParameters(NotificationAuthenticationSessionRespons throw new UnprocessableSmartIdResponseException("VC value is missing from the response"); } } + + private String encodeInteractionsToBase64(List interactions) { + try { + var mapper = new ObjectMapper(); + return Base64.getEncoder().encodeToString(mapper.writeValueAsString(interactions).getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + throw new SmartIdClientException("Unable to encode interactions to base64", e); + } + } } diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index ff23bdc0..9a190334 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -35,6 +35,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.Interaction; @@ -254,7 +255,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { if (signableHash != null || signableData != null) { signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); } - signatureProtocolParameters.setSignatureAlgorithm(SignatureUtil.getSignatureAlgorithm(signatureAlgorithm, signableHash, signableData)); + signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setNonce(nonce); request.setAllowedInteractionsOrder(allowedInteractionsOrder); diff --git a/src/main/java/ee/sk/smartid/RandomChallenge.java b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java similarity index 86% rename from src/main/java/ee/sk/smartid/RandomChallenge.java rename to src/main/java/ee/sk/smartid/RpChallengeGenerator.java index 758a5004..0a466a7a 100644 --- a/src/main/java/ee/sk/smartid/RandomChallenge.java +++ b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java @@ -31,20 +31,20 @@ import org.bouncycastle.util.encoders.Base64; /** - * Utility class for generating random challenges in Base64 format + * Utility class for generating RP challenges in Base64 format */ -public class RandomChallenge { +public class RpChallengeGenerator { private static final int MAX_LENGTH = 64; private static final int MIN_LENGTH = 32; - private RandomChallenge() { + private RpChallengeGenerator() { } /** - * Generates a random challenge with max length of 64 bytes + * Generates a RP challenge with a maximum length of 64 bytes * - * @return random challenge in Base64 format + * @return RP challenge in Base64 format */ public static String generate() { byte[] randBytes = new byte[MAX_LENGTH]; @@ -53,10 +53,10 @@ public static String generate() { } /** - * Generates a random challenge with specified length + * Generates a RP challenge with specified length * * @param length length of the challenge - * @return random challenge in Base64 format + * @return RP challenge in Base64 format */ public static String generate(int length) { if (length < MIN_LENGTH || length > MAX_LENGTH) { diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index 5c2ff4dc..4621070a 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -28,9 +28,7 @@ public enum SignatureAlgorithm { - SHA256WITHRSA("sha256WithRSAEncryption"), - SHA384WITHRSA("sha384WithRSAEncryption"), - SHA512WITHRSA("sha512WithRSAEncryption"); + RSASSA_PSS("rsassa-pss"); private final String algorithmName; diff --git a/src/main/java/ee/sk/smartid/SignatureProtocol.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java index 42dd88c5..111dcdae 100644 --- a/src/main/java/ee/sk/smartid/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -27,6 +27,6 @@ */ public enum SignatureProtocol { - ACSP_V1, + ACSP_V2, RAW_DIGEST_SIGNATURE } diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index e5989d51..7970d924 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -68,9 +68,9 @@ public class SmartIdClient { private SessionStatusPoller sessionStatusPoller; /** - * Creates a new builder for creating a dynamic link certificate choice session request. + * Creates a new builder for creating a device link certificate choice session request. * - * @return a builder for creating a new dynamic link certificate choice session request + * @return a builder for creating a new device link certificate choice session request */ public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertificateRequest() { return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) @@ -90,12 +90,12 @@ public NotificationCertificateChoiceSessionRequestBuilder createNotificationCert } /** - * Creates a new builder for creating a new dynamic link authentication session request + * Creates a new builder for creating a new device link authentication session request * - * @return builder for creating a new dynamic link authentication session request + * @return builder for creating a new device link authentication session request */ - public DynamicLinkAuthenticationSessionRequestBuilder createDynamicLinkAuthentication() { - return new DynamicLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) + public DeviceLinkAuthenticationSessionRequestBuilder createDeviceLinkAuthentication() { + return new DeviceLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) .withRelyingPartyUUID(relyingPartyUUID) .withRelyingPartyName(relyingPartyName); } @@ -151,8 +151,8 @@ public SessionStatusPoller getSessionStatusPoller() { * * @return DynamicLinkRequestBuilder */ - public DynamicContentBuilder createDynamicContent() { - return new DynamicContentBuilder(); + public DeviceLinkBuilder createDynamicContent() { + return new DeviceLinkBuilder(); } /** diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index bb347330..0160718b 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -35,7 +35,7 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -67,7 +67,7 @@ public interface SmartIdConnector extends Serializable { * @param request CertificateChoiceSessionRequest containing necessary parameters * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request); + DeviceLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request); /** * Initiates a notification based certificate choice request. @@ -94,7 +94,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a dynamic link based signature sessions. @@ -103,7 +103,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret */ - DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); + DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); /** * Initiates a notification-based signature session using a semantics identifier. @@ -131,30 +131,30 @@ public interface SmartIdConnector extends Serializable { void setSslContext(SSLContext sslContext); /** - * Create anonymous authentication session with dynamic link + * Create anonymous authentication session with device link * - * @param authenticationRequest The dynamic link authentication session request - * @return The dynamic link authentication session response + * @param authenticationRequest The device link authentication session request + * @return The device link authentication session response */ - DynamicLinkSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest); + DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest); /** - * Create authentication session with dynamic link using semantics identifier + * Create authentication session with device link using semantics identifier * - * @param authenticationRequest The dynamic link authentication session request + * @param authenticationRequest The device link authentication session request * @param semanticsIdentifier The semantics identifier - * @return The dynamic link authentication session response + * @return The device link authentication session response */ - DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); /** - * Create authentication session with dynamic link using document number + * Create authentication session with device link using document number * - * @param authenticationRequest The dynamic link authentication session request + * @param authenticationRequest The device link authentication session request * @param documentNumber The document number - * @return The dynamic link authentication session response + * @return The device link authentication session response */ - DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); + DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); /** * Create authentication session with notification using semantics identifier diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index fd697917..3461d48d 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -47,7 +47,7 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -86,9 +86,9 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; - private static final String ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH = "authentication/dynamic-link/anonymous"; - private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/dynamic-link/etsi"; - private static final String DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/dynamic-link/document"; + private static final String ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH = "authentication/device-link/anonymous"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/device-link/etsi"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/device-link/document"; private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; @@ -128,32 +128,32 @@ public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundEx } @Override - public DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { logger.debug("Starting dynamic link authentication session with semantics identifier"); URI uri = UriBuilder.fromUri(endpointUrl) - .path(DYNAMIC_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); + return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); } @Override - public DynamicLinkSessionResponse initDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { - logger.debug("Starting dynamic link authentication session with document number"); + public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { + logger.debug("Starting device link authentication session with document number"); URI uri = UriBuilder.fromUri(endpointUrl) - .path(DYNAMIC_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); + return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); } @Override - public DynamicLinkSessionResponse initAnonymousDynamicLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { - logger.debug("Starting anonymous dynamic link authentication session"); + public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { + logger.debug("Starting anonymous device link authentication session"); URI uri = UriBuilder.fromUri(endpointUrl) - .path(ANONYMOUS_DYNAMIC_LINK_AUTHENTICATION_PATH) + .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) .build(); - return postDynamicLinkAuthenticationRequest(uri, authenticationRequest); + return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); } @Override @@ -177,7 +177,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public DynamicLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request) { + public DeviceLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request) { logger.debug("Initiating dynamic link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) @@ -208,7 +208,7 @@ public NotificationCertificateChoiceSessionResponse initNotificationCertificateC } @Override - public DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -218,7 +218,7 @@ public DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionReque } @Override - public DynamicLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { + public DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) @@ -296,9 +296,9 @@ protected String getJdkMajorVersion() { } } - private DynamicLinkSessionResponse postDynamicLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { + private DeviceLinkSessionResponse postDeviceLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkSessionResponse.class); + return postRequest(uri, request, DeviceLinkSessionResponse.class); } catch (NotFoundException e) { logger.warn("User account not found for URI " + uri, e); throw new UserAccountNotFoundException(); @@ -320,9 +320,9 @@ private NotificationAuthenticationSessionResponse postNotificationAuthentication } } - private DynamicLinkSessionResponse postDynamicLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { + private DeviceLinkSessionResponse postDynamicLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkSessionResponse.class); + return postRequest(uri, request, DeviceLinkSessionResponse.class); } catch (NotFoundException ex) { logger.warn("User account not found for URI {}", uri, ex); throw new UserAccountNotFoundException(); @@ -344,9 +344,9 @@ private NotificationCertificateChoiceSessionResponse postNotificationCertificate } } - private DynamicLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { + private DeviceLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { - return postRequest(uri, request, DynamicLinkSessionResponse.class); + return postRequest(uri, request, DeviceLinkSessionResponse.class); } catch (NotFoundException ex) { logger.warn("User account not found for URI " + uri, ex); throw new UserAccountNotFoundException(); diff --git a/src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java similarity index 70% rename from src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java rename to src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index 81fdc7d8..bf1763e9 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AcspV1SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java @@ -28,17 +28,18 @@ import java.io.Serializable; -public class AcspV1SignatureProtocolParameters implements Serializable { +public class AcspV2SignatureProtocolParameters implements Serializable { - private String randomChallenge; + private String rpChallenge; private String signatureAlgorithm; + private SignatureAlgorithmParameters signatureAlgorithmParameters; - public String getRandomChallenge() { - return randomChallenge; + public String getRpChallenge() { + return rpChallenge; } - public void setRandomChallenge(String randomChallenge) { - this.randomChallenge = randomChallenge; + public void setRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; } public String getSignatureAlgorithm() { @@ -48,4 +49,12 @@ public String getSignatureAlgorithm() { public void setSignatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } + + public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java index 03101122..2c2caaf5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -27,7 +27,6 @@ */ import java.io.Serializable; -import java.util.List; import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; @@ -42,14 +41,11 @@ public class AuthenticationSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String certificateLevel; - private final SignatureProtocol signatureProtocol = SignatureProtocol.ACSP_V1; + private final SignatureProtocol signatureProtocol = SignatureProtocol.ACSP_V2; - private AcspV1SignatureProtocolParameters acspV1SignatureProtocolParameters; + private AcspV2SignatureProtocolParameters acspV2SignatureProtocolParameters; - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - private List allowedInteractionsOrder; + private String interactions; @JsonInclude(JsonInclude.Include.NON_NULL) private RequestProperties requestProperties; @@ -57,6 +53,9 @@ public class AuthenticationSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_NULL) private Set capabilities; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String initialCallbackURL; + public String getRelyingPartyUUID() { return relyingPartyUUID; } @@ -85,28 +84,20 @@ public SignatureProtocol getSignatureProtocol() { return signatureProtocol; } - public AcspV1SignatureProtocolParameters getSignatureProtocolParameters() { - return acspV1SignatureProtocolParameters; - } - - public void setSignatureProtocolParameters(AcspV1SignatureProtocolParameters acspV1SignatureProtocolParameters) { - this.acspV1SignatureProtocolParameters = acspV1SignatureProtocolParameters; + public AcspV2SignatureProtocolParameters getSignatureProtocolParameters() { + return acspV2SignatureProtocolParameters; } - public String getNonce() { - return nonce; + public void setSignatureProtocolParameters(AcspV2SignatureProtocolParameters acspV2SignatureProtocolParameters) { + this.acspV2SignatureProtocolParameters = acspV2SignatureProtocolParameters; } - public void setNonce(String nonce) { - this.nonce = nonce; - } + public String getInteractions() { + return interactions; - public List getAllowedInteractionsOrder() { - return allowedInteractionsOrder; } - - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; + public void setInteractions(String interactions) { + this.interactions = interactions; } public RequestProperties getRequestProperties() { @@ -124,4 +115,12 @@ public Set getCapabilities() { public void setCapabilities(Set capabilities) { this.capabilities = capabilities; } + + public String getInitialCallbackURL() { + return initialCallbackURL; + } + + public void setInitialCallbackURL(String initialCallbackURL) { + this.initialCallbackURL = initialCallbackURL; + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java similarity index 73% rename from src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java rename to src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java index b8201a1a..6868084b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteraction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java @@ -26,23 +26,26 @@ * #L% */ -import static ee.sk.smartid.rest.dao.DynamicLinkInteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.rest.dao.DynamicLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; +import static ee.sk.smartid.rest.dao.DeviceLinkInteractionFlow.CONFIRMATION_MESSAGE; +import static ee.sk.smartid.rest.dao.DeviceLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; -public class DynamicLinkInteraction extends Interaction { +public class DeviceLinkInteraction extends Interaction { - private DynamicLinkInteraction(DynamicLinkInteractionFlow type) { + public DeviceLinkInteraction() { + } + + private DeviceLinkInteraction(DeviceLinkInteractionFlow type) { this.type = type; } - public static DynamicLinkInteraction displayTextAndPIN(String displayText60) { - var interaction = new DynamicLinkInteraction(DISPLAY_TEXT_AND_PIN); + public static DeviceLinkInteraction displayTextAndPIN(String displayText60) { + var interaction = new DeviceLinkInteraction(DISPLAY_TEXT_AND_PIN); interaction.displayText60 = displayText60; return interaction; } - public static DynamicLinkInteraction confirmationMessage(String displayText200) { - var interaction = new DynamicLinkInteraction(CONFIRMATION_MESSAGE); + public static DeviceLinkInteraction confirmationMessage(String displayText200) { + var interaction = new DeviceLinkInteraction(CONFIRMATION_MESSAGE); interaction.displayText200 = displayText200; return interaction; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java similarity index 93% rename from src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java rename to src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java index 78f27843..38e4a644 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkInteractionFlow.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java @@ -28,14 +28,14 @@ import com.fasterxml.jackson.annotation.JsonValue; -public enum DynamicLinkInteractionFlow implements InteractionFlow { +public enum DeviceLinkInteractionFlow implements InteractionFlow { DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), CONFIRMATION_MESSAGE("confirmationMessage"); private final String code; - DynamicLinkInteractionFlow(String code) { + DeviceLinkInteractionFlow(String code) { this.code = code; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java similarity index 86% rename from src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java rename to src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index 937d949e..5795270c 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DynamicLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -27,13 +27,14 @@ */ import java.io.Serializable; +import java.net.URI; import java.time.Instant; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) -public class DynamicLinkSessionResponse implements Serializable { +public class DeviceLinkSessionResponse implements Serializable { @JsonProperty(access = JsonProperty.Access.READ_ONLY) private final Instant receivedAt; @@ -41,8 +42,9 @@ public class DynamicLinkSessionResponse implements Serializable { private String sessionID; private String sessionToken; private String sessionSecret; + private URI deviceLinkBase; - public DynamicLinkSessionResponse() { + public DeviceLinkSessionResponse() { receivedAt = Instant.now(); } @@ -73,4 +75,12 @@ public void setSessionSecret(String sessionSecret) { public Instant getReceivedAt() { return receivedAt; } + + public URI getDeviceLinkBase() { + return deviceLinkBase; + } + + public void setDeviceLinkBase(URI deviceLinkBase) { + this.deviceLinkBase = deviceLinkBase; + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java new file mode 100644 index 00000000..4054853c --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java @@ -0,0 +1,49 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public enum HashAlgorithm implements Serializable { + + SHA_256("SHA-256"), + SHA_384("SHA-384"), + SHA_512("SHA-512"), + SHA3_256("SHA3-256"), + SHA3_384("SHA3-384"), + SHA3_512("SHA3-512"); + + private final String value; + + HashAlgorithm(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index 1af72165..f9b4cbe3 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -41,7 +41,7 @@ public InteractionFlow getType() { return type; } - public void setType(DynamicLinkInteractionFlow type) { + public void setType(DeviceLinkInteractionFlow type) { this.type = type; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java new file mode 100644 index 00000000..4f8e1974 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -0,0 +1,42 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class SignatureAlgorithmParameters implements Serializable { + + private HashAlgorithm hashAlgorithm; + + public HashAlgorithm getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java b/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java new file mode 100644 index 00000000..878cea9b --- /dev/null +++ b/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java @@ -0,0 +1,50 @@ +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +public class DeviceLinkUtil { + + private static final ObjectMapper mapper = new ObjectMapper(); + + public static String encodeToBase64(List interactions) { + try { + String json = mapper.writeValueAsString(interactions); + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + throw new SmartIdClientException("Unable to encode interactions to base64", e); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureUtil.java b/src/main/java/ee/sk/smartid/util/SignatureUtil.java similarity index 64% rename from src/main/java/ee/sk/smartid/SignatureUtil.java rename to src/main/java/ee/sk/smartid/util/SignatureUtil.java index 3e0fe555..ab1017b1 100644 --- a/src/main/java/ee/sk/smartid/SignatureUtil.java +++ b/src/main/java/ee/sk/smartid/util/SignatureUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.util; /*- * #%L @@ -26,6 +26,8 @@ * #L% */ +import ee.sk.smartid.SignableData; +import ee.sk.smartid.SignableHash; import ee.sk.smartid.exception.permanent.SmartIdClientException; public class SignatureUtil { @@ -42,24 +44,4 @@ public static String getDigestToSignBase64(SignableHash signableHash, SignableDa throw new SmartIdClientException("Either signableHash or signableData must be set."); } } - - public static String getSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm, SignableHash signableHash, SignableData signableData) { - if (signatureAlgorithm != null) { - return signatureAlgorithm.getAlgorithmName(); - } else if (signableHash != null && signableHash.getHashType() != null) { - return getAlgorithmFromHashType(signableHash.getHashType()); - } else if (signableData != null && signableData.getHashType() != null) { - return getAlgorithmFromHashType(signableData.getHashType()); - } else { - return SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - } - } - - private static String getAlgorithmFromHashType(HashType hashType) { - return switch (hashType) { - case SHA256 -> SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(); - case SHA384 -> SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(); - case SHA512 -> SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(); - }; - } } diff --git a/src/test/java/ee/sk/smartid/AuthCodeTest.java b/src/test/java/ee/sk/smartid/AuthCodeTest.java index df990210..2f630813 100644 --- a/src/test/java/ee/sk/smartid/AuthCodeTest.java +++ b/src/test/java/ee/sk/smartid/AuthCodeTest.java @@ -54,8 +54,8 @@ class AuthCodeTest { @ParameterizedTest @ArgumentsSource(AuthCodeArgumentsProvider.class) - void createHash(DynamicLinkType dynamicLinkType, SessionType sessionType, String expectedPayload) { - String authCodeInBase64 = AuthCode.createHash(dynamicLinkType, sessionType, 1, toBase64("sessionSecret")); + void createHash(DeviceLinkType deviceLinkType, SessionType sessionType, String expectedPayload) { + String authCodeInBase64 = AuthCode.createHash(deviceLinkType, sessionType, 1, toBase64("sessionSecret")); String expected = hashThePayload(expectedPayload); assertEquals(expected, authCodeInBase64); @@ -69,7 +69,7 @@ void createHash_dynamicLinkTypeNotProvided_throwException() { @Test void createHash_sessionTypeNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DynamicLinkType.QR_CODE, null, 1, "sessionSecret")); + var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DeviceLinkType.QR_CODE, null, 1, "sessionSecret")); assertEquals("Session type must be set", ex.getMessage()); } @@ -118,17 +118,17 @@ private static class AuthCodeArgumentsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, "QR.auth.1"), - Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.AUTHENTICATION, "Web2App.auth.1"), - Arguments.of(DynamicLinkType.APP_2_APP, SessionType.AUTHENTICATION, "App2App.auth.1"), + Arguments.of(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, "QR.auth.1"), + Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.AUTHENTICATION, "Web2App.auth.1"), + Arguments.of(DeviceLinkType.APP_2_APP, SessionType.AUTHENTICATION, "App2App.auth.1"), - Arguments.of(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, "QR.sign.1"), - Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.SIGNATURE, "Web2App.sign.1"), - Arguments.of(DynamicLinkType.APP_2_APP, SessionType.SIGNATURE, "App2App.sign.1"), + Arguments.of(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, "QR.sign.1"), + Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.SIGNATURE, "Web2App.sign.1"), + Arguments.of(DeviceLinkType.APP_2_APP, SessionType.SIGNATURE, "App2App.sign.1"), - Arguments.of(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, "QR.cert.1"), - Arguments.of(DynamicLinkType.WEB_2_APP, SessionType.CERTIFICATE_CHOICE, "Web2App.cert.1"), - Arguments.of(DynamicLinkType.APP_2_APP, SessionType.CERTIFICATE_CHOICE, "App2App.cert.1") + Arguments.of(DeviceLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, "QR.cert.1"), + Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.CERTIFICATE_CHOICE, "Web2App.cert.1"), + Arguments.of(DeviceLinkType.APP_2_APP, SessionType.CERTIFICATE_CHOICE, "App2App.cert.1") ); } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java index 2b97fbc7..c1b8ae79 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java @@ -152,7 +152,7 @@ void from_signatureIsNotProvided_throwException() { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); assertEquals("Signature parameter is missing in session status", exception.getMessage()); @@ -168,7 +168,7 @@ void from_signatureValueIsNotProvided_throwException(String signatureValue) { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); @@ -186,7 +186,7 @@ void from_serverRandomIsNotProvided_throwException(String serverRandom) { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); @@ -201,7 +201,7 @@ void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorit var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); @@ -215,7 +215,7 @@ void from_sessionCertificateIsNotProvided_throwException() { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); @@ -233,7 +233,7 @@ void from_certificateValueIsNotProvided_throwException(String certificateValue) var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); @@ -250,7 +250,7 @@ void from_certificateLevelIsNotProvided_throwException(String certificateLevel) var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); @@ -267,7 +267,7 @@ void from_interactionFlowUsedNotProvided_throwException(String interactionFlowUs var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); sessionStatus.setInteractionFlowUsed(interactionFlowUsed); @@ -284,7 +284,7 @@ void from_certificateIsInvalid_throwException() { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); @@ -318,7 +318,7 @@ private static SessionCertificate toSessionCertificate(String AUTH_CERT, String private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V1"); + sessionStatus.setSignatureProtocol("ACSP_V2"); sessionStatus.setSignature(sessionSignature); sessionStatus.setCert(sessionCertificate); sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 66e43a9c..75581e9a 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -63,23 +63,23 @@ void setUp() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - dynamicLinkAuthenticationResponse.setEndResult("OK"); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + deviceLinkAuthenticationResponse.setEndResult("OK"); - dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); - dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); + deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values - dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values + deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest"); + authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -91,25 +91,25 @@ void toAuthenticationIdentity() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - dynamicLinkAuthenticationResponse.setEndResult("OK"); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + deviceLinkAuthenticationResponse.setEndResult("OK"); - dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); - dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); + deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values - dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values + deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, + authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, AuthenticationCertificateLevel.ADVANCED, - "randomChallengeFromTestUserAuthRequest"); + "rpChallengeFromTestUserAuthRequest"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -121,25 +121,25 @@ void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidateCertificateLevel_ok() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - dynamicLinkAuthenticationResponse.setEndResult("OK"); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + deviceLinkAuthenticationResponse.setEndResult("OK"); - dynamicLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - dynamicLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - dynamicLinkAuthenticationResponse.setHashType(HashType.SHA512); - dynamicLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); + deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); + deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); + deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - // TODO - 04.12.24: if dynamic-link authentication can be completed with test number then replace these values - dynamicLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - dynamicLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); + // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values + deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); + deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, + authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, null, - "randomChallengeFromTestUserAuthRequest"); + "rpChallengeFromTestUserAuthRequest"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -151,140 +151,140 @@ void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidate @Test void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndInvalidSignature_throwsException() { var validator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureData")); - dynamicLinkAuthenticationResponse.setServerRandom("serverRandom"); - dynamicLinkAuthenticationResponse.setEndResult("OK"); + deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureData")); + deviceLinkAuthenticationResponse.setServerRandom("serverRandom"); + deviceLinkAuthenticationResponse.setEndResult("OK"); var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> validator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + () -> validator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallenge")); assertEquals("Signature verification failed", ex.getMessage()); } @Test void toAuthenticationIdentity_certificateHasMatchingKeyButDifferentDN_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForFakeCert")); - dynamicLinkAuthenticationResponse.setServerRandom("serverRandom"); - dynamicLinkAuthenticationResponse.setEndResult("OK"); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForFakeCert")); + deviceLinkAuthenticationResponse.setServerRandom("serverRandom"); + deviceLinkAuthenticationResponse.setEndResult("OK"); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> - authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallenge")); + authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallenge")); assertEquals("Signer's certificate is not trusted", exception.getMessage()); } @Test - void toAuthenticationIdentity_dynamicLinkAuthenticationResponseIsMissing_throwException() { + void toAuthenticationIdentity_deviceLinkAuthenticationResponseIsMissing_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(null, null)); - assertEquals("Dynamic link authentication response is not provided", exception.getMessage()); + assertEquals("Device link authentication response is not provided", exception.getMessage()); } @Test - void toAuthenticationIdentity_randomChallengeIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, null)); + void toAuthenticationIdentity_rpChallengeIsNotProvided_throwException() { + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, null)); - assertEquals("Random challenge is not provided", exception.getMessage()); + assertEquals("RP challenge is not provided", exception.getMessage()); } @Test void toAuthenticationIdentity_certificateValueIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Certificate is not provided", exception.getMessage()); } @Test void toAuthenticationIdentity_expiredCertificateProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(EXPIRED_CERT)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(EXPIRED_CERT)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Signer's certificate is not valid", exception.getMessage()); } @Test void toAuthenticationIdentity_certificateIsNotTrusted_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Signer's certificate is not trusted", exception.getMessage()); } @Test void toAuthenticationIdentity_certificateLevelIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(null); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(null); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Certificate level is not provided", exception.getMessage()); } @Test void toAuthenticationIdentity_certificateLevelIsLowerThanRequested_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.ADVANCED); - var exception = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.ADVANCED); + var exception = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Signer's certificate is below requested certificate level", exception.getMessage()); } @Test void toAuthenticationIdentity_algorithmNameIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Algorithm name is not provided", exception.getMessage()); } @Test void toAuthenticationIdentity_signatureValueIsNotProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Signature value is not provided", exception.getMessage()); } @Test void toAuthenticationIdentity_invalidAlgorithmNameIsProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("invalidAlgorithmName"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("invalidAlgorithmName"); + deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Invalid signature algorithm was provided", exception.getMessage()); } @Test void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Signature verification failed", exception.getMessage()); } @@ -292,12 +292,12 @@ void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { @Disabled("Do not have necessary test data to make this work.") @Test void toAuthenticationIdentity_signatureDoesNotMatch_throwException() { - var dynamicLinkAuthenticationResponse = new AuthenticationResponse(); - dynamicLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - dynamicLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - dynamicLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - dynamicLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("bXhY5CO3gxQ2hxnuQm0Lm/4fXoFPogy4LwS6d0aUu9sZjCfNV5n6IUse45UYLhvmfK4NW5QarlYRTEqIYxlVQ0UMFm6WXQA5AHeOu/JoxKQDnbSeH8Y9FADOnqYXbPWz0W4aFVo0JFoMPO2JrwjC3rFrfded0EkD76vrazzwZxWNkWskC3jJq2Dgu3tsuDdv+Q4moNJYamADQtxYc7a16GNUEklo/ZlUS1pFanDplWTIwGaJd+ZWCvqPrz7cr+PObYfv4NsSN1QBij+eYDS+o6pTK/Ba/ve9AmdR4zS7dv/i1paSmGx3kbm/N0fNn+gelgPv8poOat1TGadT5FLEXWdytDW6I7S+d80xiInPHwKeXI4G4DL+F6zdRw8zWvR6ziXHIkxh/LnioRnoKxOiQZbQbrws2exjyFAS2HkX5UHugPfOkK0YSrJHVpwkOarDAvj7RoOHTFxLd/6FKbugDTG+0tIY4W6RROENePjZW+1eJIOkivO7/iHv3Qi6iIPhW9fB7XUDEtOdmmSrnheU6S9lvKnFYoW3Wcjy12bpK9QoaIzUykzQpO6maOxGr7nQv20AdM6y0vI16Y/8GIEqrGf9V/XVvv5SZFX3BPT3sAsBj0C18imfyyqhU33y1Gr/xMAc0Qbf4Cs92SLczY5yzd1BKGeM3ajaSaHRZbtjRdfiP7xyedyVyWF8COOHVfZb4cXwdpIbtXFkWNcYrfSnhLsRenhIrbKmiDsPRRZCZW8tpDWhr7ge2KY8wb1SbOa38WiNXTjNJAuviZ4ZmUOl5y4CrESdPXN7x7qH+jmfzxUSvBFYaSY2ey46ShHr9zQj7kz3NajIztGK7//sMnQsXuToUnSc5H0XwEwVUT6kSS6ZVYe58quDOgD47Dtj8wczXx081LSXAJXJ75XfxcwJhNn78oHVOR6EqTjOmRLlqj12Bw0WjhzIaut4wQdx0eTXGLqwn6b3RrVoVuwhJ2kwkURe0WDoKa7AWqYZBCHjGlgB3fNEBCNdKLw5ji+0C0jO")); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(dynamicLinkAuthenticationResponse, "randomChallengeFromTestUserAuthRequest")); + var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); + deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); + deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); + deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("bXhY5CO3gxQ2hxnuQm0Lm/4fXoFPogy4LwS6d0aUu9sZjCfNV5n6IUse45UYLhvmfK4NW5QarlYRTEqIYxlVQ0UMFm6WXQA5AHeOu/JoxKQDnbSeH8Y9FADOnqYXbPWz0W4aFVo0JFoMPO2JrwjC3rFrfded0EkD76vrazzwZxWNkWskC3jJq2Dgu3tsuDdv+Q4moNJYamADQtxYc7a16GNUEklo/ZlUS1pFanDplWTIwGaJd+ZWCvqPrz7cr+PObYfv4NsSN1QBij+eYDS+o6pTK/Ba/ve9AmdR4zS7dv/i1paSmGx3kbm/N0fNn+gelgPv8poOat1TGadT5FLEXWdytDW6I7S+d80xiInPHwKeXI4G4DL+F6zdRw8zWvR6ziXHIkxh/LnioRnoKxOiQZbQbrws2exjyFAS2HkX5UHugPfOkK0YSrJHVpwkOarDAvj7RoOHTFxLd/6FKbugDTG+0tIY4W6RROENePjZW+1eJIOkivO7/iHv3Qi6iIPhW9fB7XUDEtOdmmSrnheU6S9lvKnFYoW3Wcjy12bpK9QoaIzUykzQpO6maOxGr7nQv20AdM6y0vI16Y/8GIEqrGf9V/XVvv5SZFX3BPT3sAsBj0C18imfyyqhU33y1Gr/xMAc0Qbf4Cs92SLczY5yzd1BKGeM3ajaSaHRZbtjRdfiP7xyedyVyWF8COOHVfZb4cXwdpIbtXFkWNcYrfSnhLsRenhIrbKmiDsPRRZCZW8tpDWhr7ge2KY8wb1SbOa38WiNXTjNJAuviZ4ZmUOl5y4CrESdPXN7x7qH+jmfzxUSvBFYaSY2ey46ShHr9zQj7kz3NajIztGK7//sMnQsXuToUnSc5H0XwEwVUT6kSS6ZVYe58quDOgD47Dtj8wczXx081LSXAJXJ75XfxcwJhNn78oHVOR6EqTjOmRLlqj12Bw0WjhzIaut4wQdx0eTXGLqwn6b3RrVoVuwhJ2kwkURe0WDoKa7AWqYZBCHjGlgB3fNEBCNdKLw5ji+0C0jO")); + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); assertEquals("Failed to verify validity of signature returned by Smart-ID", exception.getMessage()); } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java new file mode 100644 index 00000000..276637a9 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -0,0 +1,626 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.HashAlgorithm; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; + +class DeviceLinkAuthenticationSessionRequestBuilderTest { + + private static final String BASE64_PATTERN = "^[A-Za-z0-9+/]+={0,2}$"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + void initAuthenticationSession_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); + assertEquals("DEMO", request.getRelyingPartyName()); + assertEquals("QUALIFIED", request.getCertificateLevel()); + assertEquals(SignatureProtocol.ACSP_V2, request.getSignatureProtocol()); + assertNotNull(request.getSignatureProtocolParameters()); + assertNotNull(request.getSignatureProtocolParameters().getRpChallenge()); + assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertNotNull(request.getInteractions()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); + + DeviceLinkInteraction[] parsed = parseInteractionsFromBase64(request.getInteractions()); + assertTrue(Stream.of(parsed).anyMatch(i -> i.getType().is("displayTextAndPIN"))); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withSignatureAlgorithm(signatureAlgorithm) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); + } + + @Test + void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNull(request.getRequestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withShareMdClientIpAddress(ipRequested) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.getRequestProperties()); + assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withCapabilities(capabilities) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.getCapabilities()); + } + + @Test + void initAuthenticationSession_initialCallbackUrlIsValid_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withInitialCallbackURL("https://valid.example.com/path") + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + AuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("https://valid.example.com/path", request.getInitialCallbackURL()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName("DEMO") + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName(relyingPartyName) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter rpChallenge must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(rpChallenge) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withSignatureAlgorithm(null) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession()); + assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(interactions) + .initAuthenticationSession()); + assertEquals("Parameter interactions must be set", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateInteractionsProvider.class) + void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(duplicateInteractions) + .initAuthenticationSession()); + + assertEquals("Duplicate values in interactions are not allowed", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInteractionsProvider.class) + public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(DeviceLinkInteraction interaction, String expectedException) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(interaction)) + .initAuthenticationSession()); + assertEquals(expectedException, exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log in"))) + .withInitialCallbackURL(url) + .initAuthenticationSession() + ); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(null) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession() + ); + assertEquals("Parameter hashAlgorithm must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(null) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession() + ); + + assertEquals("Parameter hashAlgorithm must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .initAuthenticationSession() + ); + + assertEquals("Only one of semanticsIdentifier or documentNumber may be set", exception.getMessage()); + } + + private DeviceLinkInteraction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { + byte[] decodedBytes = Base64.decode(base64EncodedJson); + String json = new String(decodedBytes, StandardCharsets.UTF_8); + var mapper = new ObjectMapper(); + return mapper.readValue(json, DeviceLinkInteraction[].class); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session ID is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); + deviceLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + deviceLinkAuthenticationSessionResponse.setSessionToken(sessionToken); + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session token is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); + dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); + dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + + initAuthentication(); + }); + assertEquals("Session secret is missing from the response", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("00000000-0000-0000-0000-000000000000"); + response.setSessionToken(generateBase64String("sessionToken")); + response.setSessionSecret(generateBase64String("sessionSecret")); + response.setDeviceLinkBase(deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + + when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(response); + initAuthentication(); + }); + + assertEquals("deviceLinkBase is missing or empty in the response", exception.getMessage()); + } + + private void initAuthentication() { + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .initAuthenticationSession(); + } + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @Test + void initAuthenticationSession_withDocumentNumber() { + when(connector.initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), any(String.class))) + .thenReturn(createDynamicLinkAuthenticationResponse()); + + new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + .withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .initAuthenticationSession(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + private DeviceLinkSessionResponse createDynamicLinkAuthenticationResponse() { + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); + deviceLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + deviceLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); + deviceLinkAuthenticationSessionResponse.setSessionSecret(generateBase64String("sessionSecret")); + deviceLinkAuthenticationSessionResponse.setDeviceLinkBase(URI.create("https://example.com/callback")); + return deviceLinkAuthenticationSessionResponse; + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class CapabilitiesArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[0], Collections.emptySet()), + Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), + Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) + ); + } + } + + private static class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Parameter rpChallenge is not a valid Base64 encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(30).getBytes())), + "Encoded rpChallenge must be between 44 and 88 characters"), + Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(67).getBytes())), + "Encoded rpChallenge must be between 44 and 88 characters") + ); + } + } + + private static class DuplicateInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + var interaction1 = DeviceLinkInteraction.displayTextAndPIN("Log in."); + var interaction2 = DeviceLinkInteraction.displayTextAndPIN("Log in."); + + return Stream.of( + Arguments.of(List.of(interaction1, interaction1)), + Arguments.of(List.of(interaction1, interaction2)) + ); + } + } + + private static class InvalidInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided text is null", DeviceLinkInteraction.displayTextAndPIN(null)), + "displayText60 cannot be null for AllowedInteractionOrder of type DISPLAY_TEXT_AND_PIN"), + Arguments.of(Named.of("provided text is longer than allowed 60", DeviceLinkInteraction.displayTextAndPIN("a".repeat(61))), + "displayText60 must not be longer than 60 characters"), + Arguments.of(Named.of("provided text is null", DeviceLinkInteraction.confirmationMessage(null)), + "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE"), + Arguments.of(Named.of("provided text is longer than allowed 200", DeviceLinkInteraction.confirmationMessage("a".repeat(201))), + "displayText200 must not be longer than 200 characters") + ); + } + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java similarity index 78% rename from src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java rename to src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index 77d126c3..e61a3113 100644 --- a/src/test/java/ee/sk/smartid/DynamicContentBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -48,50 +48,50 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; -class DynamicContentBuilderTest { +class DeviceLinkBuilderTest { @Nested class CreateUri { @ParameterizedTest @EnumSource - void createUri_forDifferentDynamicLinks(DynamicLinkType dynamicLinkType) { + void createUri_forDifferentDynamicLinks(DeviceLinkType deviceLinkType) { long elapsedSeconds = 1L; - URI uri = new DynamicContentBuilder() + URI uri = new DeviceLinkBuilder() .withBaseUrl("https://smart-id.com/dynamic-link/") .withVersion("0.1") - .withDynamicLinkType(dynamicLinkType) + .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(elapsedSeconds) - .withAuthCode(AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, elapsedSeconds, toBase64("sessionSecret"))) + .withAuthCode(AuthCode.createHash(deviceLinkType, SessionType.AUTHENTICATION, elapsedSeconds, toBase64("sessionSecret"))) .createUri(); - assertUri(uri, dynamicLinkType, SessionType.AUTHENTICATION); + assertUri(uri, deviceLinkType, SessionType.AUTHENTICATION); } @ParameterizedTest @EnumSource void createUri_withSessionType(SessionType sessionType) { long elapsedSeconds = 1L; - URI uri = new DynamicContentBuilder() + URI uri = new DeviceLinkBuilder() .withBaseUrl("https://smart-id.com/dynamic-link/") .withVersion("0.1") - .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(sessionType) .withSessionToken("sessionToken") .withElapsedSeconds(elapsedSeconds) - .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, elapsedSeconds, toBase64("sessionSecret"))) + .withAuthCode(AuthCode.createHash(DeviceLinkType.QR_CODE, sessionType, elapsedSeconds, toBase64("sessionSecret"))) .createUri(); - assertUri(uri, DynamicLinkType.QR_CODE, sessionType); + assertUri(uri, DeviceLinkType.QR_CODE, sessionType); } @ParameterizedTest @NullAndEmptySource void createUri_baseUrlIsOverriddenToBeEmpty_throwException(String baseUrl) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() + () -> new DeviceLinkBuilder() .withBaseUrl(baseUrl) .createUri()); assertEquals("Parameter baseUrl must be set", ex.getMessage()); @@ -101,7 +101,7 @@ void createUri_baseUrlIsOverriddenToBeEmpty_throwException(String baseUrl) { @NullAndEmptySource void createUri_versionIsOverriddenToBeEmpty_throwException(String version) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() + () -> new DeviceLinkBuilder() .withVersion(version) .createUri()); assertEquals("Parameter version must be set", ex.getMessage()); @@ -110,8 +110,8 @@ void createUri_versionIsOverriddenToBeEmpty_throwException(String version) { @Test void createUri_dynamicLinkTypeIsNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(null) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(null) .createUri()); assertEquals("Parameter dynamicLinkType must be set", ex.getMessage()); } @@ -119,8 +119,8 @@ void createUri_dynamicLinkTypeIsNotProvided_throwException() { @Test void createUri_sessionTypeIsNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(null) .createUri()); assertEquals("Parameter sessionType must be set", ex.getMessage()); @@ -130,8 +130,8 @@ void createUri_sessionTypeIsNotProvided_throwException() { @NullAndEmptySource void createUri_sessionTokenIsEmpty_throwException(String sessionToken) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(sessionToken) .createUri()); @@ -141,8 +141,8 @@ void createUri_sessionTokenIsEmpty_throwException(String sessionToken) { @Test void createUri_elapsedSecondsNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(null) @@ -154,8 +154,8 @@ void createUri_elapsedSecondsNotProvided_throwException() { @NullAndEmptySource void createUri_userLanguageIsEmpty_throwException(String userLanguage) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(1L) @@ -168,8 +168,8 @@ void createUri_userLanguageIsEmpty_throwException(String userLanguage) { @NullAndEmptySource void createUri_authCodeIsEmpty_throwException(String authCode) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(1L) @@ -185,37 +185,37 @@ class CreateQrCode { @ParameterizedTest @EnumSource void createQrCode_forDifferentSessionsTypes(SessionType sessionType) { - String qrDataUri = new DynamicContentBuilder() + String qrDataUri = new DeviceLinkBuilder() .withBaseUrl("https://smart-id.com/dynamic-link/") .withVersion("0.1") - .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(sessionType) .withSessionToken("sessionToken") .withElapsedSeconds(1L) - .withAuthCode(AuthCode.createHash(DynamicLinkType.QR_CODE, sessionType, 1, toBase64("sessionSecret"))) + .withAuthCode(AuthCode.createHash(DeviceLinkType.QR_CODE, sessionType, 1, toBase64("sessionSecret"))) .createQrCodeDataUri(); String[] qrDataUriParts = qrDataUri.split(","); URI uri = URI.create(QrCodeUtil.extractQrContent(qrDataUriParts[1]).getText()); - assertUri(uri, DynamicLinkType.QR_CODE, sessionType); + assertUri(uri, DeviceLinkType.QR_CODE, sessionType); } @ParameterizedTest - @EnumSource(value = DynamicLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createQrCode_wrongLinkTypeIsBeingUsed_throwException(DynamicLinkType notSupportedDynamicLinkType) { + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createQrCode_wrongLinkTypeIsBeingUsed_throwException(DeviceLinkType notSupportedDeviceLinkType) { var ex = assertThrows(SmartIdClientException.class, - () -> new DynamicContentBuilder() - .withDynamicLinkType(notSupportedDynamicLinkType) + () -> new DeviceLinkBuilder() + .withDeviceLinkType(notSupportedDeviceLinkType) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("sessionToken") .withElapsedSeconds(1L) .withAuthCode("authCode") .createQrCodeDataUri()); - assertEquals("Dynamic link type must be QR_CODE", ex.getMessage()); + assertEquals("Device link type must be QR_CODE", ex.getMessage()); } } - private static void assertUri(URI uri, DynamicLinkType qrCode, SessionType sessionType) { + private static void assertUri(URI uri, DeviceLinkType qrCode, SessionType sessionType) { assertThat(uri.getScheme(), equalTo("https")); assertThat(uri.getHost(), equalTo("smart-id.com")); assertThat(uri.getPath(), equalTo("/dynamic-link/")); diff --git a/src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java deleted file mode 100644 index 51f499ce..00000000 --- a/src/test/java/ee/sk/smartid/DynamicLinkAuthenticationSessionRequestBuilderTest.java +++ /dev/null @@ -1,507 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; - -class DynamicLinkAuthenticationSessionRequestBuilderTest { - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Nested - class ValidateRequiredRequestParameters { - - @Test - void initAuthenticationSession_ok() { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); - assertEquals("DEMO", request.getRelyingPartyName()); - assertEquals("QUALIFIED", request.getCertificateLevel()); - assertEquals(SignatureProtocol.ACSP_V1, request.getSignatureProtocol()); - assertNotNull(request.getSignatureProtocolParameters()); - assertNotNull(request.getSignatureProtocolParameters().getRandomChallenge()); - assertEquals("sha512WithRSAEncryption", request.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertNotNull(request.getAllowedInteractionsOrder()); - assertTrue(request.getAllowedInteractionsOrder().stream().anyMatch(interaction -> interaction.getType().is("displayTextAndPIN"))); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(certificateLevel) - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.getCertificateLevel()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initAuthenticationSession_nonce_ok(String nonce) { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withNonce(nonce) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.getNonce()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withSignatureAlgorithm(signatureAlgorithm) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); - } - - @Test - void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNull(request.getRequestProperties()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withShareMdClientIpAddress(ipRequested) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.getRequestProperties()); - assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withCapabilities(capabilities) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initAnonymousDynamicLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.getCapabilities()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName(relyingPartyName) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals("Parameter randomChallenge must be set", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidRandomChallengeArgumentProvider.class) - void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withSignatureAlgorithm(null) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidNonceProvider.class) - void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withNonce(invalidNonce) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(interactions) - .initAuthenticationSession()); - assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInteractionsProvider.class) - public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(DynamicLinkInteraction interaction, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(List.of(interaction)) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); - - initAuthentication(); - }); - assertEquals("Session ID is missing from the response", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { - var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionResponse.setSessionToken(sessionToken); - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); - - initAuthentication(); - }); - assertEquals("Session token is missing from the response", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { - var exception = assertThrows(SmartIdClientException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); - dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); - when(connector.initAnonymousDynamicLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); - - initAuthentication(); - }); - assertEquals("Session secret is missing from the response", exception.getMessage()); - } - - private void initAuthentication() { - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - } - } - - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @Test - void initAuthenticationSession_withDocumentNumber() { - when(connector.initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), any(String.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DynamicLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(DynamicLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initDynamicLinkAuthentication(any(AuthenticationSessionRequest.class), documentNumberCaptor.capture()); - String capturedDocumentNumber = documentNumberCaptor.getValue(); - - assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); - } - - private DynamicLinkSessionResponse createDynamicLinkAuthenticationResponse() { - var dynamicLinkAuthenticationSessionResponse = new DynamicLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); - dynamicLinkAuthenticationSessionResponse.setSessionSecret(generateBase64String("sessionSecret")); - return dynamicLinkAuthenticationSessionResponse; - } - - private static String generateBase64String(String text) { - return Base64.toBase64String(text.getBytes()); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } - - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[0], Collections.emptySet()), - Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), - Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) - ); - } - } - - private static class InvalidRandomChallengeArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Parameter randomChallenge is not a valid Base64 encoded string"), - Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(31).getBytes())), - "Size of parameter randomChallenge must be between 32 and 64 bytes"), - Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(65).getBytes())), - "Size of parameter randomChallenge must be between 32 and 64 bytes") - ); - } - } - - private static class InvalidNonceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), - Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") - ); - } - } - - private static class InvalidInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided text is null", DynamicLinkInteraction.displayTextAndPIN(null)), - "displayText60 cannot be null for AllowedInteractionOrder of type DISPLAY_TEXT_AND_PIN"), - Arguments.of(Named.of("provided text is longer than allowed 60", DynamicLinkInteraction.displayTextAndPIN("a".repeat(61))), - "displayText60 must not be longer than 60 characters"), - Arguments.of(Named.of("provided text is null", DynamicLinkInteraction.confirmationMessage(null)), - "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE"), - Arguments.of(Named.of("provided text is longer than allowed 200", DynamicLinkInteraction.confirmationMessage("a".repeat(201))), - "displayText200 must not be longer than 200 characters") - ); - } - } -} diff --git a/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java index 6b603f17..02e498f1 100644 --- a/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java @@ -43,7 +43,7 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; class DynamicLinkCertificateChoiceSessionRequestBuilderTest { @@ -65,7 +65,7 @@ void setUp() { void initiateCertificateChoice() { when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkSessionResponse result = builderService.initCertificateChoice(); + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); @@ -80,7 +80,7 @@ void initiateCertificateChoice_nullRequestProperties() { builderService.withShareMdClientIpAddress(false); when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkSessionResponse result = builderService.initCertificateChoice(); + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); @@ -95,7 +95,7 @@ void initiateCertificateChoice_missingCertificateLevel() { builderService.withCertificateLevel(null); when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkSessionResponse result = builderService.initCertificateChoice(); + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); @@ -106,7 +106,7 @@ void initiateCertificateChoice_withValidCapabilities() { builderService.withCapabilities("ADVANCED", "QUALIFIED"); when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkSessionResponse result = builderService.initCertificateChoice(); + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); @@ -121,7 +121,7 @@ void initiateCertificateChoice_nullCapabilities() { builderService.withCapabilities(); when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DynamicLinkSessionResponse result = builderService.initCertificateChoice(); + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); assertEquals("test-session-id", result.getSessionID()); @@ -144,7 +144,7 @@ void initiateCertificateChoice_whenResponseIsNull() { @Test void initiateCertificateChoice_whenSessionIDIsNull() { - var responseWithNullSessionID = new DynamicLinkSessionResponse(); + var responseWithNullSessionID = new DeviceLinkSessionResponse(); responseWithNullSessionID.setSessionToken("test-session-token"); responseWithNullSessionID.setSessionSecret("test-session-secret"); when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); @@ -194,8 +194,8 @@ void initiateCertificateChoice_emptyNonce() { } } - private static DynamicLinkSessionResponse mockCertificateChoiceResponse() { - var response = new DynamicLinkSessionResponse(); + private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { + var response = new DeviceLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); diff --git a/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java index b312e1f3..a11b2f25 100644 --- a/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java @@ -36,12 +36,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.net.URI; import java.util.Base64; import java.util.List; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -57,10 +59,11 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.SignatureSessionRequest; +@Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-105") class DynamicLinkSignatureSessionRequestBuilderTest { private SmartIdConnector connector; @@ -73,7 +76,7 @@ void setUp() { builder = new DynamicLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())) .withCertificateChoiceMade(false); } @@ -85,12 +88,13 @@ void initSignatureSession_withSemanticsIdentifier() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); assertEquals("test-session-id", signature.getSessionID()); assertEquals("test-session-token", signature.getSessionToken()); assertEquals("test-session-secret", signature.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); @@ -105,12 +109,13 @@ void initSignatureSession_withDocumentNumber() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); assertEquals("test-session-id", signature.getSessionID()); assertEquals("test-session-token", signature.getSessionToken()); assertEquals("test-session-secret", signature.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(documentNumber)); @@ -125,7 +130,7 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -144,7 +149,7 @@ void initSignatureSession_withNonce_ok(String nonce) { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -162,7 +167,7 @@ void initSignatureSession_withRequestProperties() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -179,11 +184,11 @@ void initSignatureSession_withRequestProperties() { void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { var signableData = new SignableData("Test data".getBytes()); signableData.setHashType(HashType.SHA256); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -191,7 +196,7 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @@ -206,14 +211,14 @@ void initSignatureSession_withSignableHash(HashType hashType) { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @@ -225,7 +230,7 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); @@ -242,18 +247,18 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { var signableData = new SignableData("Test data".getBytes()); signableData.setHashType(hashType); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA256WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @@ -267,14 +272,14 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(mockSignatureSessionResponse()); - DynamicLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); } @Nested @@ -318,7 +323,7 @@ void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlg @ParameterizedTest @NullAndEmptySource - void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { + void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { builder.withAllowedInteractionsOrder(allowedInteractionsOrder); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); @@ -373,7 +378,7 @@ class ResponseValidationTests { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionID(String sessionID) { - var response = new DynamicLinkSessionResponse(); + var response = new DeviceLinkSessionResponse(); response.setSessionID(sessionID); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); @@ -388,7 +393,7 @@ void validateResponseParameters_missingSessionID(String sessionID) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionToken(String sessionToken) { - var response = new DynamicLinkSessionResponse(); + var response = new DeviceLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken(sessionToken); response.setSessionSecret("test-session-secret"); @@ -403,7 +408,7 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionSecret(String sessionSecret) { - var response = new DynamicLinkSessionResponse(); + var response = new DeviceLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret(sessionSecret); @@ -416,11 +421,12 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { } } - private DynamicLinkSessionResponse mockSignatureSessionResponse() { - var response = new DynamicLinkSessionResponse(); + private DeviceLinkSessionResponse mockSignatureSessionResponse() { + var response = new DeviceLinkSessionResponse(); response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); return response; } diff --git a/src/test/java/ee/sk/smartid/InteractionUtil.java b/src/test/java/ee/sk/smartid/InteractionUtil.java new file mode 100644 index 00000000..9e643aee --- /dev/null +++ b/src/test/java/ee/sk/smartid/InteractionUtil.java @@ -0,0 +1,50 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.util.encoders.Base64; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class InteractionUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private InteractionUtil() { + } + + public static String encodeInteractionsAsBase64(List interactions) { + try { + String json = objectMapper.writeValueAsString(interactions); + return Base64.toBase64String(json.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new RuntimeException("Failed to encode interactions to Base64", e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index 4337c0fc..e6af731a 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -94,10 +94,10 @@ void initAuthenticationSession_ok() { assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); assertEquals("DEMO", request.getRelyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V1, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.ACSP_V2, request.getSignatureProtocol()); assertNotNull(request.getSignatureProtocolParameters()); - assertEquals("sha512WithRSAEncryption", request.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertNotNull(request.getAllowedInteractionsOrder()); + assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertNotNull(request.getInteractions()); } @ParameterizedTest @@ -142,26 +142,6 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); } - @Test - void initAuthenticationSession_withNonce() { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withNonce("uniqueNonce") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("uniqueNonce", request.getNonce()); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index 0bf5d788..092a610b 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -42,6 +42,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -63,6 +64,7 @@ import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; +@Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") class NotificationSignatureSessionRequestBuilderTest { private SmartIdConnector connector; @@ -160,7 +162,7 @@ void initSignatureSession_withNonce_ok(String nonce) { @Test void withSignatureAlgorithm_setsCorrectAlgorithm() { var signableData = new SignableData("Test data".getBytes()); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); @@ -171,7 +173,7 @@ void withSignatureAlgorithm_setsCorrectAlgorithm() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); } @@ -240,7 +242,7 @@ void initSignatureSession_withCapabilities(Set capabilities, Set void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { var signableData = new SignableData("Test data".getBytes()); signableData.setHashType(hashType); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.SHA256WITHRSA).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); @@ -251,7 +253,7 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA256WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); } @@ -271,7 +273,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); } @Test @@ -287,7 +289,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignableDataOrHash() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); } @Nested diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index b45c8ae6..e15ef21f 100644 --- a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -154,8 +154,8 @@ void convertToBase64() { } private static URI createUri() { - return new DynamicContentBuilder() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + return new DeviceLinkBuilder() + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") .withAuthCode("Y7jBVqtP_KcY4GyJ0gTK717wZnfRLvondEUjjCRJAsQ") diff --git a/src/test/java/ee/sk/smartid/RandomChallengeTest.java b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java similarity index 86% rename from src/test/java/ee/sk/smartid/RandomChallengeTest.java rename to src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java index 06d55431..216e9df9 100644 --- a/src/test/java/ee/sk/smartid/RandomChallengeTest.java +++ b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java @@ -35,11 +35,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -class RandomChallengeTest { +class RpChallengeGeneratorTest { @Test void generate_defaultValueUsed() { - String challenge = RandomChallenge.generate(); + String challenge = RpChallengeGenerator.generate(); assertNotNull(challenge); byte[] decodeChallenge = Base64.decode(challenge); @@ -49,7 +49,7 @@ void generate_defaultValueUsed() { @ParameterizedTest @ValueSource(ints = {32, 43, 59, 64}) void generate_providedValuesAreInAllowedRange(int allowedValue) { - String challenge = RandomChallenge.generate(allowedValue); + String challenge = RpChallengeGenerator.generate(allowedValue); assertNotNull(challenge); byte[] decodeChallenge = Base64.decode(challenge); assertEquals(allowedValue, decodeChallenge.length); @@ -57,12 +57,12 @@ void generate_providedValuesAreInAllowedRange(int allowedValue) { @Test void generate_providedValueIsLessThanAllowed_throwException() { - assertThrows(IllegalArgumentException.class, () -> RandomChallenge.generate(31)); + assertThrows(IllegalArgumentException.class, () -> RpChallengeGenerator.generate(31)); } @Test void generate_providedValueIsMoreThanAllowed_throwException() { - assertThrows(IllegalArgumentException.class, () -> RandomChallenge.generate(65)); + assertThrows(IllegalArgumentException.class, () -> RpChallengeGenerator.generate(65)); } } diff --git a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java index ee92718a..8963eaae 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java @@ -162,7 +162,7 @@ class SignatureValidation { @Test void from_validRawDigestSignature() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); SignatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); @@ -172,7 +172,7 @@ void from_validRawDigestSignature() { @ParameterizedTest @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) void from_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); SignatureResponse response = SignatureResponseMapper.from(sessionStatus, certificateLevel.name()); diff --git a/src/test/java/ee/sk/smartid/SignatureUtilTest.java b/src/test/java/ee/sk/smartid/SignatureUtilTest.java index 3f7e9336..07005219 100644 --- a/src/test/java/ee/sk/smartid/SignatureUtilTest.java +++ b/src/test/java/ee/sk/smartid/SignatureUtilTest.java @@ -32,10 +32,9 @@ import java.util.Base64; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.SignatureUtil; class SignatureUtilTest { @@ -83,57 +82,6 @@ void getDigestToSignBase64_withSignableHashFieldsNotFilled() { assertEquals("Either signableHash or signableData must be set.", exception.getMessage()); } - @Test - void getSignatureAlgorithm_withExplicitSignatureAlgorithm() { - String algorithm = SignatureUtil.getSignatureAlgorithm(SignatureAlgorithm.SHA384WITHRSA, null, null); - assertEquals(SignatureAlgorithm.SHA384WITHRSA.getAlgorithmName(), algorithm); - } - - @Test - void getSignatureAlgorithm_withSignableHashHashTypeNull() { - var signableHash = new SignableHash(); - signableHash.setHash("Test hash".getBytes()); - signableHash.setHashType(null); - - String algorithm = SignatureUtil.getSignatureAlgorithm(null, signableHash, null); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); - } - - @ParameterizedTest - @EnumSource(HashType.class) - void getSignatureAlgorithm_withHashTypeInSignableHash(HashType hashType) { - var signableHash = new SignableHash(); - signableHash.setHashType(hashType); - - String algorithm = SignatureUtil.getSignatureAlgorithm(null, signableHash, null); - assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", algorithm); - } - - @ParameterizedTest - @EnumSource(HashType.class) - void getSignatureAlgorithm_withHashTypeInSignableData(HashType hashType) { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(hashType); - - String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, signableData); - assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", algorithm); - } - - @Test - void getSignatureAlgorithm_withSignableDataHashTypeNull() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(null); - - String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, signableData); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); - } - - @Test - void getSignatureAlgorithm_withDefaultAlgorithm() { - String algorithm = SignatureUtil.getSignatureAlgorithm(null, null, null); - assertEquals(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName(), algorithm); - } - @Test void setHashInBase64_shouldDecodeBase64String() { var signableHash = new SignableHash(); diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 2ebc24ee..8d8c214e 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -38,15 +38,17 @@ import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -76,7 +78,7 @@ class DynamicLinkCertificateChoiceSession { void createDynamicLinkCertificateChoice() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -123,16 +125,17 @@ void createNotificationCertificateChoice_withDocumentNumber() { @Nested @WireMockTest(httpPort = 18089) - class DynamicLinkAuthenticationSession { + class DeviceLinkAuthenticationSession { @Test - void createDynamicLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); - - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + void createDeviceLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); assertNotNull(response.getSessionID()); @@ -142,14 +145,15 @@ void createDynamicLinkAuthentication_anonymous() { } @Test - void createDynamicLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); + void createDeviceLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); assertNotNull(response.getSessionID()); @@ -159,14 +163,15 @@ void createDynamicLinkAuthentication_withDocumentNumber() { } @Test - void createDynamicLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/etsi/PNOEE-1234567890", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); + void createDeviceLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); assertNotNull(response.getSessionID()); @@ -176,22 +181,23 @@ void createDynamicLinkAuthentication_withSemanticsIdentifier() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-105") @Nested @WireMockTest(httpPort = 18089) class DynamicLinkSignatureSession { @Test void createDynamicLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/dynamic-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -203,16 +209,16 @@ void createDynamicLinkSignature_withDocumentNumber() { @Test void createDynamicLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "requests/dynamic-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "requests/device-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -223,6 +229,7 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18089) class NotificationAuthenticationSession { @@ -260,6 +267,7 @@ void createNotificationAuthentication_withDocumentNumber() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18089) class NotificationBasedSignatureSession { @@ -273,7 +281,7 @@ void createNotificationSignature_withDocumentNumber() { NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -296,7 +304,7 @@ void createNotificationSignature_withSemanticsIdentifier() { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -339,19 +347,20 @@ class DynamicContent { @ParameterizedTest @EnumSource - void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/dynamic-link/anonymous", "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); - - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkAuthentication() - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA) - .withAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))) + void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA_512) .initAuthenticationSession(); long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); + String authCode = AuthCode.createHash(deviceLinkType, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(dynamicLinkType) + .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) @@ -359,23 +368,24 @@ void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DynamicLin .withAuthCode(authCode) .createUri(); - assertUri(qrCodeUri, SessionType.AUTHENTICATION, dynamicLinkType, response.getSessionToken()); + assertUri(qrCodeUri, SessionType.AUTHENTICATION, deviceLinkType, response.getSessionToken()); } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") @ParameterizedTest @EnumSource - void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DynamicLinkType dynamicLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); + void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(dynamicLinkType, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); + String authCode = AuthCode.createHash(deviceLinkType, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); URI qrCodeUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(dynamicLinkType) + .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) @@ -383,22 +393,23 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(Dynamic .withAuthCode(authCode) .createUri(); - assertUri(qrCodeUri, SessionType.CERTIFICATE_CHOICE, dynamicLinkType, response.getSessionToken()); + assertUri(qrCodeUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); - DynamicLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) @@ -408,17 +419,17 @@ void createDynamicContent_createQrCode() { String[] qrCodeDataUriParts = qrCodeDataUri.split(","); URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - assertUri(uri, SessionType.CERTIFICATE_CHOICE, DynamicLinkType.QR_CODE, response.getSessionToken()); + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.getSessionToken()); } - private static void assertUri(URI qrCodeUri, SessionType sessionType, DynamicLinkType dynamicLinkType, String sessionToken) { + private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { assertEquals("https", qrCodeUri.getScheme()); assertEquals("smart-id.com", qrCodeUri.getHost()); - assertEquals("/dynamic-link/", qrCodeUri.getPath()); + assertEquals("/device-link/", qrCodeUri.getPath()); assertTrue(qrCodeUri.getQuery().contains("version=0.1")); assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("dynamicLinkType=" + dynamicLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("dynamicLinkType=" + deviceLinkType.getValue())); assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); assertTrue(qrCodeUri.getQuery().contains("elapsedSeconds=")); assertTrue(qrCodeUri.getQuery().contains("lang=eng")); diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index a410c50e..029fccf9 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -80,7 +80,7 @@ public static void stubForbiddenResponse(String url, String requestFile) { public static void stubErrorResponse(String url, String requestFile, int errorStatus) { stubFor(post(urlEqualTo(url)) .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile))) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) .willReturn(aResponse() .withStatus(errorStatus) .withHeader("Content-Type", "application/json") @@ -99,7 +99,7 @@ public static void stubRequestWithResponse(String urlEquals, String responseFile public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { stubFor(post(urlEqualTo(url)) .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile))) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 2b7cf058..38cbb0ae 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -49,6 +49,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.rest.dao.SemanticsIdentifier; @@ -60,16 +61,15 @@ import ee.sk.smartid.CertificateChoiceResponse; import ee.sk.smartid.CertificateChoiceResponseMapper; import ee.sk.smartid.CertificateLevel; -import ee.sk.smartid.DynamicLinkType; -import ee.sk.smartid.RandomChallenge; +import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; import ee.sk.smartid.SignableData; import ee.sk.smartid.SignatureResponse; import ee.sk.smartid.SignatureResponseMapper; import ee.sk.smartid.SmartIdClient; import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -103,18 +103,18 @@ class DynamicLinkExamples { @Test void anonymousAuthentication_withApp2App() { // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RandomChallenge.generate(); + String randomChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response - DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient - .createDynamicLinkAuthentication() + DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number - .withRandomChallenge(randomChallenge) + .withRpChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( + .withInteractions(Collections.singletonList( // before the user can enter PIN. If user selects wrong verification code then the operation will fail. - DynamicLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPIN("Log in?") )) .initAuthenticationSession(); @@ -132,10 +132,10 @@ void anonymousAuthentication_withApp2App() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); // Generate dynamic link URI dynamicLink = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link + .withDeviceLinkType(DeviceLinkType.APP_2_APP) // specify the type of dynamic link .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for .withSessionToken(sessionToken) // provide token from sessions response .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time @@ -175,17 +175,17 @@ void authentication_withSemanticIdentifierAndQrCode() { "40504040001"); // identifier (according to country and identity type reference) // For security reasons a new random challenge must be created for each new authentication request - String randomChallenge = RandomChallenge.generate(); + String randomChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response - DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient - .createDynamicLinkAuthentication() + DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") + .withRpChallenge(randomChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") )) // we want to get the IP address of the device running Smart-ID app // for the IP to be returned the service provider (SK) must switch on this option @@ -205,10 +205,10 @@ void authentication_withSemanticIdentifierAndQrCode() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for .withSessionToken(sessionToken) // provide token from sessions response .withElapsedSeconds(elapsedSeconds) @@ -243,17 +243,17 @@ void authentication_withDocumentNumberAndQrCode() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new random challenge must be created for each new authentication request - String randomChallenge = RandomChallenge.generate(); + String randomChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response - DynamicLinkSessionResponse authenticationSessionResponse = smartIdClient - .createDynamicLinkAuthentication() + DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") + .withRpChallenge(randomChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") )) // we want to get the IP address of the device running Smart-ID app // for the IP to be returned the service provider (SK) must switch on this option @@ -273,10 +273,10 @@ void authentication_withDocumentNumberAndQrCode() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for .withSessionToken(sessionToken) // provide token from sessions response .withElapsedSeconds(elapsedSeconds) @@ -334,14 +334,14 @@ void signature_withDocumentNumber() { signableData.setHashType(HashType.SHA512); // Build the dynamic link signature request - DynamicLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) .withRelyingPartyName(smartIdClient.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withDocumentNumber(documentNumber) .withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .initSignatureSession(); // Process the signature response @@ -357,10 +357,10 @@ void signature_withDocumentNumber() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for .withSessionToken(sessionToken) // provide token from sessions response .withElapsedSeconds(elapsedSeconds) @@ -416,14 +416,14 @@ void signature_withSemanticIdentifier() { "40504040001"); // identifier (according to country and identity type reference) // Build the dynamic link signature request - DynamicLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) .withRelyingPartyName(smartIdClient.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) .withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .initSignatureSession(); // Process the signature response @@ -440,10 +440,10 @@ void signature_withSemanticIdentifier() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); + String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error + .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for .withSessionToken(sessionToken) // provide token from sessions response .withElapsedSeconds(elapsedSeconds) @@ -476,7 +476,7 @@ void authentication_withDocumentNumber() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RandomChallenge.generate(); + String randomChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -503,7 +503,7 @@ void authentication_withDocumentNumber() { assertEquals("COMPLETE", sessionStatus.getState()); assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); // validate sessions status result and map session status to authentication response AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); @@ -529,7 +529,7 @@ void authentication_withSemanticIdentifier() { "40504040001"); // identifier (according to country and identity type reference) // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RandomChallenge.generate(); + String randomChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -556,7 +556,7 @@ void authentication_withSemanticIdentifier() { assertEquals("COMPLETE", sessionStatus.getState()); assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); // validate sessions status result and map session status to authentication response AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index d3abbec7..7787bd96 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -41,17 +41,18 @@ import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; +import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.RandomChallenge; +import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SignatureAlgorithm; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.SmartIdRestConnector; -import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -88,10 +89,10 @@ class DynamicLink { class Authentication { @Test - void initAnonymousDynamicLinkAuthentication() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + void initAnonymousDeviceLinkAuthentication() { + AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDynamicLinkAuthentication(request); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -100,10 +101,10 @@ void initAnonymousDynamicLinkAuthentication() { } @Test - void initDynamicLinkAuthentication_withDocumentNumber() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + void initDeviceLinkAuthentication_withDocumentNumber() { + AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -112,10 +113,10 @@ void initDynamicLinkAuthentication_withDocumentNumber() { } @Test - void initDynamicLinkAuthentication_withSemanticsIdentifier() { - AuthenticationSessionRequest request = toDynamicLinkAuthenticationSessionRequest(); + void initDeviceLinkAuthentication_withSemanticsIdentifier() { + AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - DynamicLinkSessionResponse sessionResponse = smartIdConnector.initDynamicLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionResponse.getSessionToken())); @@ -123,16 +124,16 @@ void initDynamicLinkAuthentication_withSemanticsIdentifier() { assertNotNull(sessionResponse.getReceivedAt()); } - private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { var request = new AuthenticationSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - var signatureParameters = new AcspV1SignatureProtocolParameters(); - signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - signatureParameters.setRandomChallenge(RandomChallenge.generate()); + var signatureParameters = new AcspV2SignatureProtocolParameters(); + signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signatureParameters.setRpChallenge(RpChallengeGenerator.generate()); request.setSignatureProtocolParameters(signatureParameters); - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Log in?"))); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?")))); return request; } } @@ -147,7 +148,7 @@ void initDynamicLinkCertificateChoice() { request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkCertificateChoice(request); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkCertificateChoice(request); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -164,15 +165,15 @@ void initDynamicLinkSignature_withSemanticIdentifier() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign it!"))); + request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))); var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); - DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -185,15 +186,15 @@ void initDynamicLinkSignature_withDocumentNumber() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign it!"))); + request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))); var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); - DynamicLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -237,13 +238,13 @@ private static AuthenticationSessionRequest toAuthenticationRequest() { request.setRelyingPartyName(RELYING_PARTY_NAME); request.setCertificateLevel("QUALIFIED"); - String randomChallenge = RandomChallenge.generate(); - var signatureParameters = new AcspV1SignatureProtocolParameters(); - signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); - signatureParameters.setRandomChallenge(randomChallenge); + String randomChallenge = RpChallengeGenerator.generate(); + var signatureParameters = new AcspV2SignatureProtocolParameters(); + signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signatureParameters.setRpChallenge(randomChallenge); request.setSignatureProtocolParameters(signatureParameters); - request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Log in?"))); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?")))); var requestProperties = new RequestProperties(); requestProperties.setShareMdClientIpAddress(true); @@ -311,7 +312,7 @@ private static SignatureSessionRequest toSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.SHA512WITHRSA.getAlgorithmName()); + signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Sign it!"))); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 984b5927..095739f2 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -37,9 +37,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; import java.util.List; @@ -47,11 +47,13 @@ import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; @@ -60,11 +62,12 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.AcspV1SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DynamicLinkInteraction; -import ee.sk.smartid.rest.dao.DynamicLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -72,6 +75,7 @@ import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; class SmartIdRestConnectorTest { @@ -115,7 +119,7 @@ void getSessionStatus_forSuccessfulAuthenticationRequest() { assertSuccessfulResponse(sessionStatus); assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); - assertEquals("ACSP_V1", sessionStatus.getSignatureProtocol()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); assertNotNull(sessionStatus.getSignature()); assertThat(sessionStatus.getSignature().getValue(), startsWith("TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZ")); assertEquals("sha512WithRSAEncryption", sessionStatus.getSignature().getSignatureAlgorithm()); @@ -253,9 +257,9 @@ private static void assertSessionStatusErrorWithEndResult(SessionStatus sessionS @Nested @WireMockTest(httpPort = 18082) - class SemanticsIdentifierDynamicLinkAuthentication { + class SemanticsIdentifierDeviceLinkAuthentication { - private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/dynamic-link/etsi/PNOEE-48010010101"; + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; private SmartIdRestConnector connector; @@ -265,38 +269,38 @@ void setUp() { } @Test - void initDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); + void initDeviceLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-30303039914")); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test - void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + void initDeviceLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json"); + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @Test - void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + void initDeviceLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json"); + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-30303039914")); }); } } @Nested @WireMockTest(httpPort = 18083) - class DocumentNumberDynamicLinkAuthentication { + class DocumentNumberDeviceLinkAuthentication { - private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/dynamic-link/document/PNOEE-48010010101-MOCK-Q"; + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; private SmartIdRestConnector connector; @@ -306,13 +310,13 @@ void setUp() { } @Test - void initDynamicLinkAuthentication() { + void initDeviceLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/dynamic-link-authentication-session-request.json", - "responses/dynamic-link-authentication-session-response.json"); + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DynamicLinkSessionResponse response = connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-30303039914-MOCK-Q"); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); @@ -320,27 +324,27 @@ void initDynamicLinkAuthentication() { } @Test - void initDynamicLinkAuthentication_userAccountNotFound_throwException() { + void initDeviceLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/device-link-authentication-session-request.json"); + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @Test - void initDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + void initDeviceLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/device-link-authentication-session-request.json"); + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-30303039914-MOCK-Q"); }); } } @Nested @WireMockTest(httpPort = 18081) - class AnonymousDynamicLinkAuthentication { + class AnonymousDeviceLinkAuthentication { - private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/dynamic-link/anonymous"; + private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/device-link/anonymous"; private SmartIdRestConnector connector; @@ -350,33 +354,34 @@ void setUp() { } @Test - void initAnonymousDynamicLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json", "responses/dynamic-link-authentication-session-response.json"); + void initAnonymousDeviceLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DynamicLinkSessionResponse response = connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test - void initAnonymousDynamicLinkAuthentication_userAccountNotFound_throwException() { + void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json"); + connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); }); } @Test - void initAnonymousDynamicLinkAuthentication_requestIsUnauthorized_throwException() { + void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/dynamic-link-authentication-session-request.json"); - connector.initAnonymousDynamicLinkAuthentication(toDynamicLinkAuthenticationSessionRequest()); + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json"); + connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); }); } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18082) class SemanticsIdentifierNotificationAuthentication { @@ -415,6 +420,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18083) class DocumentNumberNotificationAuthentication { @@ -453,6 +459,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") @Nested @WireMockTest(httpPort = 18089) class DynamicLinkCertificateChoiceTests { @@ -472,7 +479,7 @@ void initDynamicLinkCertificateChoice() { CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); - DynamicLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); + DeviceLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); Instant end = Instant.now(); assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); @@ -674,7 +681,7 @@ void initDynamicLinkSignature_withSemanticsIdentifier_successful() { SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - DynamicLinkSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); + DeviceLinkSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); @@ -689,7 +696,7 @@ void initDynamicLinkSignature_withDocumentNumber_successful() { SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111-MOCK-Q"; - DynamicLinkSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); + DeviceLinkSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); @@ -784,6 +791,7 @@ void initDynamicLinkSignature_throwsServerMaintenanceException() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18084) class SemanticsIdentifierNotificationSignature { @@ -889,6 +897,7 @@ void initNotificationSignature_throwsServerMaintenanceException() { } } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18085) class DocumentNumberNotificationSignature { @@ -993,37 +1002,41 @@ void initNotificationSignature_throwsServerMaintenanceException() { } } - private static AuthenticationSessionRequest toDynamicLinkAuthenticationSessionRequest() { + private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); dynamicLinkAuthenticationSessionRequest.setCertificateLevel("QUALIFIED"); - var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); - signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); - signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); + signatureProtocolParameters.setRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())); + signatureProtocolParameters.setSignatureAlgorithm("rsassa-pss"); dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); - DynamicLinkInteraction interaction = DynamicLinkInteraction.displayTextAndPIN("Log in?"); - dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); + var algorithmParameters = new SignatureAlgorithmParameters(); + algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA_512); + signatureProtocolParameters.setSignatureAlgorithmParameters(algorithmParameters); + + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Log in?"); + dynamicLinkAuthenticationSessionRequest.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); return dynamicLinkAuthenticationSessionRequest; } private static AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { - var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); - dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); + var deviceLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); + deviceLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + deviceLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); - var signatureProtocolParameters = new AcspV1SignatureProtocolParameters(); - signatureProtocolParameters.setRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())); - signatureProtocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); - dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); + signatureProtocolParameters.setRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())); + signatureProtocolParameters.setSignatureAlgorithm("rsassa-pss"); + deviceLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); NotificationInteraction interaction = NotificationInteraction.verificationCodeChoice("Verify the code"); - dynamicLinkAuthenticationSessionRequest.setAllowedInteractionsOrder(List.of(interaction)); + deviceLinkAuthenticationSessionRequest.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); - return dynamicLinkAuthenticationSessionRequest; + return deviceLinkAuthenticationSessionRequest; } private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { @@ -1046,7 +1059,7 @@ private static SignatureSessionRequest createSignatureSessionRequest() { request.setSignatureProtocolParameters(protocolParameters); - request.setAllowedInteractionsOrder(List.of(DynamicLinkInteraction.displayTextAndPIN("Sign the document"))); + request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign the document"))); return request; } @@ -1066,7 +1079,7 @@ private static SignatureSessionRequest createNotificationSignatureSessionRequest return request; } - private static void assertResponseValues(DynamicLinkSessionResponse response, + private static void assertResponseValues(DeviceLinkSessionResponse response, String expectedSessionToken, String expectedSessionSecret, Instant start, @@ -1075,7 +1088,8 @@ private static void assertResponseValues(DynamicLinkSessionResponse response, assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); assertEquals(expectedSessionToken, response.getSessionToken()); assertEquals(expectedSessionSecret, response.getSessionSecret()); - assertTrue(start.isBefore(response.getReceivedAt())); - assertTrue(end.isAfter(response.getReceivedAt())); + assertNotNull(response.getReceivedAt()); + assertFalse(response.getReceivedAt().isBefore(start.minusSeconds(1))); + assertFalse(response.getReceivedAt().isAfter(end.plusSeconds(1))); } } diff --git a/src/test/resources/requests/device-link-authentication-session-request.json b/src/test/resources/requests/device-link-authentication-session-request.json new file mode 100644 index 00000000..1928fc03 --- /dev/null +++ b/src/test/resources/requests/device-link-authentication-session-request.json @@ -0,0 +1,14 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA_512" + } + }, + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +} \ No newline at end of file diff --git a/src/test/resources/requests/dynamic-link-signature-request.json b/src/test/resources/requests/device-link-signature-request.json similarity index 92% rename from src/test/resources/requests/dynamic-link-signature-request.json rename to src/test/resources/requests/device-link-signature-request.json index 0b82a790..75fae51f 100644 --- a/src/test/resources/requests/dynamic-link-signature-request.json +++ b/src/test/resources/requests/device-link-signature-request.json @@ -6,7 +6,7 @@ "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", "signatureAlgorithm": "sha512WithRSAEncryption" }, - "allowedInteractionsOrder": [ + "interactions": [ { "type": "displayTextAndPIN", "displayText60": "Sign document?" diff --git a/src/test/resources/requests/dynamic-link-authentication-session-request.json b/src/test/resources/requests/dynamic-link-authentication-session-request.json deleted file mode 100644 index cb28691c..00000000 --- a/src/test/resources/requests/dynamic-link-authentication-session-request.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V1", - "signatureProtocolParameters": { - "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "sha512WithRSAEncryption" - }, - "certificateLevel": "QUALIFIED", - "allowedInteractionsOrder": [ - { - "type": "displayTextAndPIN", - "displayText60": "Log in?" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/requests/notification-authentication-session-request.json b/src/test/resources/requests/notification-authentication-session-request.json index c110d789..c65465ed 100644 --- a/src/test/resources/requests/notification-authentication-session-request.json +++ b/src/test/resources/requests/notification-authentication-session-request.json @@ -1,12 +1,12 @@ { "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V1", + "signatureProtocol": "ACSP_V2", "signatureProtocolParameters": { - "randomChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "sha512WithRSAEncryption" + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss" }, - "allowedInteractionsOrder": [ + "interactions": [ { "type": "verificationCodeChoice", "displayText60": "Verify the code" diff --git a/src/test/resources/requests/notification-signature-session-request.json b/src/test/resources/requests/notification-signature-session-request.json index 34328744..70732ed9 100644 --- a/src/test/resources/requests/notification-signature-session-request.json +++ b/src/test/resources/requests/notification-signature-session-request.json @@ -6,7 +6,7 @@ "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", "signatureAlgorithm": "sha512WithRSAEncryption" }, - "allowedInteractionsOrder": [ + "interactions": [ { "type": "verificationCodeChoice", "displayText60": "Verify the code" diff --git a/src/test/resources/responses/dynamic-link-authentication-session-response.json b/src/test/resources/responses/device-link-authentication-session-response.json similarity index 81% rename from src/test/resources/responses/dynamic-link-authentication-session-response.json rename to src/test/resources/responses/device-link-authentication-session-response.json index f6524d55..b55ad982 100644 --- a/src/test/resources/responses/dynamic-link-authentication-session-response.json +++ b/src/test/resources/responses/device-link-authentication-session-response.json @@ -2,5 +2,6 @@ "sessionID": "00000000-0000-0000-0000-000000000000", "sessionToken": "sessionToken", "sessionSecret": "c2Vzc2lvblNlY3JldA==", + "deviceLinkBase": "https://example.org/device-link/", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-authentication.json b/src/test/resources/responses/session-status-successful-authentication.json index df8d7425..5fca38cb 100644 --- a/src/test/resources/responses/session-status-successful-authentication.json +++ b/src/test/resources/responses/session-status-successful-authentication.json @@ -5,7 +5,7 @@ "documentNumber": "PNOEE-40504040001-MOCK-Q", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "signatureProtocol": "ACSP_V1", + "signatureProtocol": "ACSP_V2", "signature": { "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", "signatureAlgorithm": "sha512WithRSAEncryption", From 0a0ba0ace8230f2cfbdd819472564f89868428ce Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Fri, 20 Jun 2025 06:32:51 +0300 Subject: [PATCH 22/57] SLIB-94 - Update generating of dynamic content (#111) * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-94 - updated generating of dynamic content * SLIB-94 - refactored device link generation * SLIB-94 - updated javadoc, exceptions * SLIB-94 - refactored, updated README * SLIB-94 - updated README, Javadocs * SLIB-94 - README and tests fixed --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 15 + README.md | 132 +++--- src/main/java/ee/sk/smartid/AuthCode.java | 107 ----- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 226 ++++++++-- ...onAuthenticationSessionRequestBuilder.java | 1 - .../smartid/SignatureAlgorithmParameters.java | 50 +++ .../java/ee/sk/smartid/SmartIdClient.java | 8 +- .../ee/sk/smartid/rest/dao/HashAlgorithm.java | 3 + .../java/ee/sk/smartid/util/StringUtil.java | 4 + src/test/java/ee/sk/smartid/AuthCodeTest.java | 135 ------ ...thenticationSessionRequestBuilderTest.java | 32 +- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 422 +++++++++++++----- .../java/ee/sk/smartid/InteractionUtil.java | 4 +- .../ee/sk/smartid/QrCodeGeneratorTest.java | 10 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 84 ++-- .../integration/ReadmeIntegrationTest.java | 240 +++++----- .../rest/SmartIdRestConnectorTest.java | 2 +- ...e-link-authentication-session-request.json | 2 +- ...-link-authentication-session-response.json | 2 +- 19 files changed, 834 insertions(+), 645 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/AuthCode.java create mode 100644 src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java delete mode 100644 src/test/java/ee/sk/smartid/AuthCodeTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8a2b3d..57bb1153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.2] - 2025-06-05 + +### Changed + +- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. +- Introduced a `DeviceLinkBuilder` to generate device-links. + - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. + - Ensures `elapsedSeconds` is only used for QR_CODE flows. + - Moved `deviceLinkBase` to required input (no more default). + - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. + - New payload structure includes required and optional fields as per documentation. + - `schemeName` is fixed to `"smart-id"`. + - Does not store `sessionSecret`, ensures it must be passed to the build method. +- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. + ## [3.1.1] - 2025-06-02 ### Changed diff --git a/README.md b/README.md index 0ad56173..18f808e4 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ This library supports Smart-ID API v3.1. * [Initiating a dynamic-link signature session using document number](#initiating-a-dynamic-link-signature-session-with-document-number) * [Examples of allowed dynamic-link interactions order](#examples-of-allowed-dynamic-link-interactions-order) * [Additional request properties](#additional-dynamic-link-session-request-properties) - * [Generating QR-code or dynamic link](#generating-qr-code-or-device-link) - * [Generating dynamic link ](#generating-dynamic-link) - * [Dynamic link parameters](#dynamic-link-parameters) + * [Generating QR-code or device link](#generating-qr-code-or-device-link) + * [Generating device link ](#generating-device-link) + * [Device link parameters](#device-link-parameters) * [Overriding default values](#overriding-default-values) * [Generating QR-code](#generating-qr-code) * [Generate QR-code Data URI](#generate-qr-code-data-uri) @@ -541,62 +541,61 @@ builder.withAllowedInteractionsOrder(List.of( ### Generating QR-code or device link Documentation to device link and QR-code requirements -https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/dynamic_link_flows.html#_dynamic_link_and_qr_presentation +https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html #### Generating device link -Dynamic link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. +Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. -##### Dynamic link parameters +##### Device link parameters -* `baseUrl`: Base URL for the dynamic link. Default value is `https://smart-id.com/dynamic-link`. -* `version`: Version of the dynamic link. Default value is `0.1`. +* `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. +* `version`: Version of the dynamic link. Only allowed value is `"1.0"`. * `deviceLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. * `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. * `sessionToken`: Token from the session response. -* `elapsedSeconds`: Elapsed time from when the session response was received. -* `userLanguage`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link -* `authCode`: Auth code is HMAC256 hash value generated from deviceLinkType, sessionType, calculated elapsed seconds since response was received and session secret. Received at and sessions secret can be found from the session response. +* `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` +* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link +* `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. +* `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. +* `initialCallbackUrl`: Optional. Initial callback URL to be used for the dynamic link. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. ```java -DynamicLinkSessionResponse sessionResponse; // response from the session initiation query. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(sessionResponse.getReceivedAt(), Instant.now()).getSeconds(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionResponse.getSessionSecret()); -// Generate dynamic link -URI dynamicLink = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link - .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createUri(); +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +// Calculate elapsed seconds since session response +long elapsedSeconds = Duration.between(session.getReceivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.getSessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withRelyingPartyName("DEMO") + .buildDeviceLink(sessionResponse.getSessionSecret()); ``` ##### Overriding default values ```java -DynamicLinkSessionResponse response; // response from the session initiation query. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); -// Generate dynamic link -URI dynamicLink = client.createDynamicContent() - .withBaseUrl("https://example.com") // override default base URL (https://smart-id.com/dynamic-link) - .withDynamicLinkType(DynamicLinkType.APP_2_APP) // specify the type of dynamic link - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(elapsedSeconds) - .withUserLanguage("est") // override default user language (eng) - .withAuthCode(authCode) - .createUri(); +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.getSessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInitialCallbackUrl("https://your-app/callback") + .buildDeviceLink(sessionResponse.getSessionSecret()); ``` #### Generating QR-code -Creating a QR code uses the Zxing library to generate a QR code image with dynamic link as content. +Creating a QR code uses the Zxing library to generate a QR code image with device link as content. According to link size the QR-code of version 9 (53x53 modules) is used. For the QR-code to be scannable by most devices the QR code module size should be ~10px. It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). @@ -605,20 +604,23 @@ Generated QR code will have error correction level low. ##### Generate QR-code Data URI ```java -DynamicLinkSessionResponse response; // response from the session initiation query. - +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); -// Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) -String qrCodeDataUri = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(response.getSessionToken()) // provide token from sessions response +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.getSessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withElapsedSeconds(elapsedSeconds) - .withAuthCode(authCode) - .createQrCodeDataUri(); + .buildDeviceLink(sessionResponse.getSessionSecret()); + +// Generate QR code image from device link URI +String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); +// Return Data URI to frontend and display the QR-code ``` ##### Generate QR-code with custom height, width, quiet area and image format @@ -630,26 +632,22 @@ Other image size in range 366px to 1159px is also possible. Width and height of The width and height of 1159px produce a QR code with a module size of 19px. ```java -DynamicLinkSessionResponse response; // response from the session initiation query. - +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); -// Generate auth code -String authCode = AuthCode.createHash(DynamicLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); -// Generate dynamic link -URI qrDynamicLink = client.createDynamicContent() - .withDynamicLinkType(DynamicLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for, possible values (auth, sign, cert) - .withSessionToken(response.getSessionToken()) // provide token from sessions response - .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createUri(); -// At this point URI can be returned to frontend and QR-code could be generated from it at frontend side. Or continue to next steps. +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.getSessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withElapsedSeconds(elapsedSeconds) + .buildDeviceLink(sessionResponse.getSessionSecret()); // Create QR-code with height and width of 570px and quiet area of 2 modules. -BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(qrDataUri, 570, 570, 2); - -// Convert BufferedImage to Data URI +BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); // Return Data URI to frontend and display the QR-code ``` diff --git a/src/main/java/ee/sk/smartid/AuthCode.java b/src/main/java/ee/sk/smartid/AuthCode.java deleted file mode 100644 index f19bf550..00000000 --- a/src/main/java/ee/sk/smartid/AuthCode.java +++ /dev/null @@ -1,107 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.StringUtil; - -/** - * This class is responsible for creating an authentication code hash for the device link. - */ -public final class AuthCode { - - private static final String PAYLOAD_FORMAT = "%s.%s.%d"; - - private AuthCode() { - } - - /** - * Creates an authentication code hash for the device link with the given time. - * - * @param deviceLinkType the type of the device link @{@link DeviceLinkType} - * @param sessionType the type of the session @{@link SessionType} - * @param elapsedSeconds the time from session creation response was received - * @param sessionSecret the session secret in Base64 format - * @return the authentication code in Base64 URL safe format - */ - public static String createHash(DeviceLinkType deviceLinkType, SessionType sessionType, long elapsedSeconds, String sessionSecret) { - validateHashingInputs(deviceLinkType, sessionType); - String payload = createPayload(deviceLinkType, sessionType, elapsedSeconds); - return hashThePayload(payload, sessionSecret); - } - - /** - * Hashes the payload with the session secret. - * - * @param payload the payload to be hashed - * @param sessionSecret the secret of the session - * @return the hashed payload in Base64 URL safe format - */ - public static String hashThePayload(String payload, String sessionSecret) { - validatePayloadInputs(payload, sessionSecret); - HMac hmac = new HMac(new SHA256Digest()); - byte[] secret = Base64.getDecoder().decode(sessionSecret.getBytes(StandardCharsets.UTF_8)); - hmac.init(new KeyParameter(secret)); - - byte[] payloadBytes = payload.getBytes(StandardCharsets.US_ASCII); - hmac.update(payloadBytes, 0, payloadBytes.length); - - byte[] result = new byte[hmac.getMacSize()]; - hmac.doFinal(result, 0); - - return Base64.getUrlEncoder().withoutPadding().encodeToString(result); - } - - private static void validateHashingInputs(DeviceLinkType deviceLinkType, SessionType sessionType) { - if (deviceLinkType == null) { - throw new SmartIdClientException("Dynamic link type must be set"); - } - if (sessionType == null) { - throw new SmartIdClientException("Session type must be set"); - } - } - - private static void validatePayloadInputs(String payload, String sessionSecret) { - if (StringUtil.isEmpty(payload)) { - throw new SmartIdClientException("Payload must be set"); - } - if (StringUtil.isEmpty(sessionSecret)) { - throw new SmartIdClientException("Session secret must be set"); - } - } - - private static String createPayload(DeviceLinkType deviceLinkType, SessionType sessionType, long elapsedSeconds) { - return String.format(PAYLOAD_FORMAT, deviceLinkType.getValue(), sessionType.getValue(), elapsedSeconds); - } -} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 5693bc1e..518251f0 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -27,46 +27,55 @@ */ import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.StringUtil; import jakarta.ws.rs.core.UriBuilder; /** - * Builds device link or QR-code. + * Builds Smart-ID device link URI. */ public class DeviceLinkBuilder { - private static final String DEFAULT_BASE_URL = "https://smart-id.com/device-link/"; - private static final String DEFAULT_VERSION = "0.1"; - private static final String DEFAULT_USER_LANGUAGE = "eng"; + private static final String ALLOWED_VERSION = "1.0"; + private static final String SCHEME_NAME = "smart-id"; - private String baseUrl = DEFAULT_BASE_URL; - private String version = DEFAULT_VERSION; + private String deviceLinkBase; + private String version = ALLOWED_VERSION; private DeviceLinkType deviceLinkType; private SessionType sessionType; private String sessionToken; private Long elapsedSeconds; - private String userLanguage = DEFAULT_USER_LANGUAGE; - private String authCode; + private String lang; + + private String digest; + private String relyingPartyNameBase64; + private String brokeredRpNameBase64; + private String interactions; + private String initialCallbackUrl; /** - * Sets the URL + * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. *

    - * Defaults to https://smart-id.com/device-link + * This is a required parameter and must be taken from the `deviceLinkBase` value received in the session-init response. * - * @param baseUrl the URL that will direct to SMART-ID application + * @param deviceLinkBase the URL that will direct to SMART-ID application * @return this builder */ - public DeviceLinkBuilder withBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; + public DeviceLinkBuilder withDeviceLinkBase(String deviceLinkBase) { + this.deviceLinkBase = deviceLinkBase; return this; } /** * Sets the version of the device link. *

    - * Defaults to 0.1 + * Only value 1.0 is allowed * * @param version the version of * @return this builder @@ -111,6 +120,7 @@ public DeviceLinkBuilder withSessionToken(String sessionToken) { /** * Sets the time passed since the session response was received. + * Only valid for QR_CODE device link type. * * @param elapsedSeconds the time passed since the session response was received in seconds * @return this builder @@ -123,70 +133,170 @@ public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { /** * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. *

    - * Defaults to "eng" + * Default value is "eng". + * The value must match the language shown to the user in the UI. + * Also used for the fallback web page if the Smart-ID app is not installed. + * + * @param lang the language of the user + * @return this builder + */ + public DeviceLinkBuilder withLang(String lang) { + this.lang = lang; + return this; + } + + /** + * Sets the digest or rpChallenge used in the session. + * Required when signatureProtocol is defined. + * + * @param digest the digest or rpChallenge value + * @return this builder + */ + public DeviceLinkBuilder withDigest(String digest) { + this.digest = digest; + return this; + } + + /** + * Sets the relying party name which will be Base64-encoded using UTF-8. + * + * @param relyingPartyName relying party name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyNameBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the brokered relying party name which will be Base64-encoded using UTF-8. + * Leave empty if not acting as a broker. + * + * @param brokeredRpName brokered RP name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { + this.brokeredRpNameBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the interactions used during session initiation as Base64 string. * - * @param userLanguage the language of the user + * @param interactions interactions string in Base64 * @return this builder */ - public DeviceLinkBuilder withUserLanguage(String userLanguage) { - this.userLanguage = userLanguage; + public DeviceLinkBuilder withInteractions(String interactions) { + this.interactions = interactions; return this; } /** - * Sets the auth code that will be used in the device link. + * Sets the callback URL used in session initiation. + * Required only for same device flows (Web2App and App2App). + * Must be left empty for QR-code flow. * - * @param authCode the auth code in the device link + * @param initialCallbackUrl initial callback URL * @return this builder */ - public DeviceLinkBuilder withAuthCode(String authCode) { - this.authCode = authCode; + public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; return this; } /** - * Creates a URI that can be used as device link or content for QR-code. + * Builds a Smart-ID device-link URI without authentication code. *

    - * To get a QR code image, use {@link #createQrCodeDataUri()} method. + * The resulting URI is used in Web2App, App2App or QR-code flows, + * and must be combined with an authCode to form a valid device-link. * - * @return URI that can be used as device link or content for QR-code + * @return unprotected device link URI */ - public URI createUri() { + public URI createUnprotectedUri() { validateInputParameters(); - return UriBuilder.fromUri(baseUrl) + UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase) .queryParam("version", version) .queryParam("sessionToken", sessionToken) - .queryParam("dynamicLinkType", deviceLinkType.getValue()) + .queryParam("deviceLinkType", deviceLinkType.getValue()) .queryParam("sessionType", sessionType.getValue()) - .queryParam("elapsedSeconds", elapsedSeconds) - .queryParam("lang", userLanguage) - .queryParam("authCode", authCode) - .build(); + .queryParam("lang", lang); + + addElapsedSecondsIfQrCode(uriBuilder); + return uriBuilder.build(); } /** - * Creates a QR code image as a Base64 encoded string. - *

    - * The device link type must be QR_CODE to create a QR code image. + * Builds the final Smart-ID device link URI by combining unprotected link and authCode. * - * @return QR code image as a Base64 encoded string + * @param sessionSecret session secret received from session initialization response. + * @return full device link URI with authCode parameter */ - public String createQrCodeDataUri() { - if (deviceLinkType != DeviceLinkType.QR_CODE) { - throw new SmartIdClientException("Device link type must be QR_CODE"); + public URI buildDeviceLink(String sessionSecret) { + URI unprotectedUri = createUnprotectedUri(); + String authCode = generateAuthCode(unprotectedUri.toString(), sessionSecret); + return UriBuilder.fromUri(unprotectedUri) + .queryParam("authCode", authCode) + .build(); + } + + private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { + if (elapsedSeconds != null) { + if (deviceLinkType != DeviceLinkType.QR_CODE) { + throw new SmartIdClientException("elapsedSeconds is only valid for QR_CODE deviceLinkType"); + } + uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); + } + } + + private String generateAuthCode(String unprotectedLink, String sessionSecret) { + validateAuthCodeParams(unprotectedLink); + return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); + } + + private String buildPayload(String unprotectedLink) { + return String.join("|", + SCHEME_NAME, + getSignatureProtocolForSession(), + StringUtil.orEmpty(digest), + relyingPartyNameBase64, + StringUtil.orEmpty(brokeredRpNameBase64), + StringUtil.orEmpty(interactions), + StringUtil.orEmpty(initialCallbackUrl), + unprotectedLink + ); + } + + private String getSignatureProtocolForSession() { + return switch (sessionType) { + case AUTHENTICATION -> SignatureProtocol.ACSP_V2.name(); + case SIGNATURE -> SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); + case CERTIFICATE_CHOICE -> ""; + }; + } + + private String calculateAuthCode(String data, String base64Key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); + byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); + } catch (Exception e) { + throw new SmartIdClientException("Failed to calculate authCode", e); } - return QrCodeGenerator.generateDataUri(createUri().toString()); } private void validateInputParameters() { - if (StringUtil.isEmpty(baseUrl)) { - throw new SmartIdClientException("Parameter baseUrl must be set"); + if (StringUtil.isEmpty(deviceLinkBase)) { + throw new SmartIdClientException("Parameter deviceLinkBase must be set"); } if (StringUtil.isEmpty(version)) { throw new SmartIdClientException("Parameter version must be set"); } + if (!ALLOWED_VERSION.equals(version)) { + throw new SmartIdClientException("Only version 1.0 is allowed"); + } if (deviceLinkType == null) { - throw new SmartIdClientException("Parameter dynamicLinkType must be set"); + throw new SmartIdClientException("Parameter deviceLinkType must be set"); } if (sessionType == null) { throw new SmartIdClientException("Parameter sessionType must be set"); @@ -194,14 +304,32 @@ private void validateInputParameters() { if (StringUtil.isEmpty(sessionToken)) { throw new SmartIdClientException("Parameter sessionToken must be set"); } - if (elapsedSeconds == null) { - throw new SmartIdClientException("Parameter elapsedSeconds must be set"); + if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { + throw new SmartIdClientException("elapsedSeconds must be set for QR_CODE deviceLinkType"); + } + if (StringUtil.isEmpty(lang)) { + throw new SmartIdClientException("Parameter lang must be set"); } - if (StringUtil.isEmpty(userLanguage)) { - throw new SmartIdClientException("Parameter userLanguage must be set"); + } + + private void validateAuthCodeParams(String unprotectedLink) { + if (StringUtil.isEmpty(relyingPartyNameBase64)) { + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + + boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); + if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { + throw new SmartIdClientException("initialCallbackUrl must be empty for QR_CODE flow"); + } + if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { + throw new SmartIdClientException("initialCallbackUrl must be provided for same-device flows"); + } + + if (sessionType != SessionType.CERTIFICATE_CHOICE && StringUtil.isEmpty(digest)) { + throw new SmartIdClientException("digest must be set for AUTH or SIGN flows"); } - if (StringUtil.isEmpty(authCode)) { - throw new SmartIdClientException("Parameter authCode must be set"); + if (StringUtil.isEmpty(unprotectedLink)) { + throw new SmartIdClientException("unprotected device-link must not be empty"); } } } diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index aae2599c..7f857d2b 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -38,7 +38,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java new file mode 100644 index 00000000..e47a299f --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java @@ -0,0 +1,50 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class SignatureAlgorithmParameters implements Serializable { + + private static final Set SUPPORTED_HASH_ALGORITHMS = Set.of("SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"); + + private String hashAlgorithm; + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(String hashAlgorithm) { + if (!SUPPORTED_HASH_ALGORITHMS.contains(hashAlgorithm)) { + throw new SmartIdClientException("Unsupported hashAlgorithm: " + hashAlgorithm + ". Supported values: " + SUPPORTED_HASH_ALGORITHMS); + } + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 7970d924..185c5cec 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -147,12 +147,14 @@ public SessionStatusPoller getSessionStatusPoller() { } /** - * Create builder for generating dynamic link or QR-code + * Create builder for generating device link or QR-code * - * @return DynamicLinkRequestBuilder + * @return DeviceLinkBuilder + * @throws SmartIdClientException if required parameters are missing or invalid */ public DeviceLinkBuilder createDynamicContent() { - return new DeviceLinkBuilder(); + return new DeviceLinkBuilder() + .withRelyingPartyName(relyingPartyName); } /** diff --git a/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java index 4054853c..d964371f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java +++ b/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java @@ -28,6 +28,8 @@ import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonValue; + public enum HashAlgorithm implements Serializable { SHA_256("SHA-256"), @@ -43,6 +45,7 @@ public enum HashAlgorithm implements Serializable { this.value = value; } + @JsonValue public String getValue() { return value; } diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index fe3991c8..1093da3a 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -36,4 +36,8 @@ public static boolean isEmpty(final CharSequence cs) { return cs == null || cs.isEmpty(); } + public static String orEmpty(String input) { + return input == null ? "" : input; + } + } diff --git a/src/test/java/ee/sk/smartid/AuthCodeTest.java b/src/test/java/ee/sk/smartid/AuthCodeTest.java deleted file mode 100644 index 2f630813..00000000 --- a/src/test/java/ee/sk/smartid/AuthCodeTest.java +++ /dev/null @@ -1,135 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.stream.Stream; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class AuthCodeTest { - - @ParameterizedTest - @ArgumentsSource(AuthCodeArgumentsProvider.class) - void createHash(DeviceLinkType deviceLinkType, SessionType sessionType, String expectedPayload) { - String authCodeInBase64 = AuthCode.createHash(deviceLinkType, sessionType, 1, toBase64("sessionSecret")); - - String expected = hashThePayload(expectedPayload); - assertEquals(expected, authCodeInBase64); - } - - @Test - void createHash_dynamicLinkTypeNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(null, SessionType.AUTHENTICATION, 1, "sessionSecret")); - assertEquals("Dynamic link type must be set", ex.getMessage()); - } - - @Test - void createHash_sessionTypeNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.createHash(DeviceLinkType.QR_CODE, null, 1, "sessionSecret")); - assertEquals("Session type must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createHash_payload_throwException(String payload) { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.hashThePayload(payload, null)); - assertEquals("Payload must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createHash_sessionSecret_throwException(String sessionSecret) { - var ex = assertThrows(SmartIdClientException.class, () -> AuthCode.hashThePayload("payload", sessionSecret)); - assertEquals("Session secret must be set", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"QR.auth.1", "QR.sign.1", "QR.cert.1"}) - void hashThePayload_validateUrlSafe(String payload) { - String sessionSecret = toBase64("sessionSecret"); - String authCodeHash = AuthCode.hashThePayload(payload, sessionSecret); - String urlSafeBase64Pattern = "^[A-Za-z0-9_-]+={0,2}$"; - assertTrue(authCodeHash.matches(urlSafeBase64Pattern)); - assertEquals(hashThePayload(payload), authCodeHash); - } - - private String hashThePayload(String payload) { - try { - byte[] keyBytes = "sessionSecret".getBytes(StandardCharsets.UTF_8); - SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256"); - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(secretKeySpec); - byte[] data = mac.doFinal(payload.getBytes(StandardCharsets.US_ASCII)); - return Base64.getUrlEncoder().withoutPadding().encodeToString(data); - } catch (InvalidKeyException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } - - private static class AuthCodeArgumentsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, "QR.auth.1"), - Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.AUTHENTICATION, "Web2App.auth.1"), - Arguments.of(DeviceLinkType.APP_2_APP, SessionType.AUTHENTICATION, "App2App.auth.1"), - - Arguments.of(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, "QR.sign.1"), - Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.SIGNATURE, "Web2App.sign.1"), - Arguments.of(DeviceLinkType.APP_2_APP, SessionType.SIGNATURE, "App2App.sign.1"), - - Arguments.of(DeviceLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, "QR.cert.1"), - Arguments.of(DeviceLinkType.WEB_2_APP, SessionType.CERTIFICATE_CHOICE, "Web2App.cert.1"), - Arguments.of(DeviceLinkType.APP_2_APP, SessionType.CERTIFICATE_CHOICE, "App2App.cert.1") - ); - } - } -} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 276637a9..374e2572 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -91,7 +91,7 @@ void initAuthenticationSession_ok() throws Exception { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); @@ -124,7 +124,7 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve .withRelyingPartyName("DEMO") .withCertificateLevel(certificateLevel) .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); @@ -145,7 +145,7 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withSignatureAlgorithm(signatureAlgorithm) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); @@ -167,7 +167,7 @@ void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_o .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession(); @@ -188,7 +188,7 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .withShareMdClientIpAddress(ipRequested) .initAuthenticationSession(); @@ -211,7 +211,7 @@ void initAuthenticationSession_capabilities_ok(String[] capabilities, Set + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withVersion(VERSION_INVALID) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Only version 1.0 is allowed", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void createUri_baseUrlIsOverriddenToBeEmpty_throwException(String baseUrl) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withBaseUrl(baseUrl) - .createUri()); - assertEquals("Parameter baseUrl must be set", ex.getMessage()); + void createUri_missingDeviceLinkBase_throwsException(String base) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(base) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter deviceLinkBase must be set", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void createUri_versionIsOverriddenToBeEmpty_throwException(String version) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() + void createUri_missingVersion_throwsException(String version) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) .withVersion(version) - .createUri()); + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); assertEquals("Parameter version must be set", ex.getMessage()); } @Test - void createUri_dynamicLinkTypeIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() + void createUri_missingDeviceLinkType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) .withDeviceLinkType(null) - .createUri()); - assertEquals("Parameter dynamicLinkType must be set", ex.getMessage()); + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter deviceLinkType must be set", ex.getMessage()); } @Test - void createUri_sessionTypeIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() + void createUri_missingSessionType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(null) - .createUri()); + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); assertEquals("Parameter sessionType must be set", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void createUri_sessionTokenIsEmpty_throwException(String sessionToken) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withDeviceLinkType(DeviceLinkType.QR_CODE) + void createUri_missingSessionToken_throwsException(String token) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(token) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .createUri()); + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); assertEquals("Parameter sessionToken must be set", ex.getMessage()); } @Test - void createUri_elapsedSecondsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withDeviceLinkType(DeviceLinkType.QR_CODE) + void createUri_missingElapsedSecondsForQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("sessionToken") - .withElapsedSeconds(null) - .createUri()); - assertEquals("Parameter elapsedSeconds must be set", ex.getMessage()); + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .createUnprotectedUri() + ); + assertEquals("elapsedSeconds must be set for QR_CODE deviceLinkType", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void createUri_userLanguageIsEmpty_throwException(String userLanguage) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withDeviceLinkType(DeviceLinkType.QR_CODE) + void createUri_missingLang_throwsException(String lang) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("sessionToken") - .withElapsedSeconds(1L) - .withUserLanguage(userLanguage) - .createUri()); - assertEquals("Parameter userLanguage must be set", ex.getMessage()); + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(lang) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter lang must be set", ex.getMessage()); } - @ParameterizedTest - @NullAndEmptySource - void createUri_authCodeIsEmpty_throwException(String authCode) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withDeviceLinkType(DeviceLinkType.QR_CODE) + @Test + void createUri_elapsedSecondsSetForNonQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("sessionToken") - .withElapsedSeconds(1L) - .withAuthCode(authCode) - .createUri()); - assertEquals("Parameter authCode must be set", ex.getMessage()); + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("elapsedSeconds is only valid for QR_CODE deviceLinkType", ex.getMessage()); } } @Nested - class CreateQrCode { + class BuildDeviceLink { @ParameterizedTest - @EnumSource - void createQrCode_forDifferentSessionsTypes(SessionType sessionType) { - String qrDataUri = new DeviceLinkBuilder() - .withBaseUrl("https://smart-id.com/dynamic-link/") - .withVersion("0.1") - .withDeviceLinkType(DeviceLinkType.QR_CODE) + @EnumSource(value = SessionType.class) + void buildDeviceLink(SessionType sessionType) { + DeviceLinkBuilder builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withSessionType(sessionType) - .withSessionToken("sessionToken") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) .withElapsedSeconds(1L) - .withAuthCode(AuthCode.createHash(DeviceLinkType.QR_CODE, sessionType, 1, toBase64("sessionSecret"))) - .createQrCodeDataUri(); + .withRelyingPartyName(RELYING_PARTY_NAME); + + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + builder.withDigest(BASE64_DIGEST); + } + + URI uri = builder.buildDeviceLink(SESSION_SECRET); - String[] qrDataUriParts = qrDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrDataUriParts[1]).getText()); - assertUri(uri, DeviceLinkType.QR_CODE, sessionType); + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); } - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createQrCode_wrongLinkTypeIsBeingUsed_throwException(DeviceLinkType notSupportedDeviceLinkType) { - var ex = assertThrows(SmartIdClientException.class, - () -> new DeviceLinkBuilder() - .withDeviceLinkType(notSupportedDeviceLinkType) + @Test + void buildDeviceLink_missingRelyingPartyName_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("sessionToken") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) .withElapsedSeconds(1L) - .withAuthCode("authCode") - .createQrCodeDataUri()); - assertEquals("Device link type must be QR_CODE", ex.getMessage()); + .withDigest(BASE64_DIGEST) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); } - } - private static void assertUri(URI uri, DeviceLinkType qrCode, SessionType sessionType) { - assertThat(uri.getScheme(), equalTo("https")); - assertThat(uri.getHost(), equalTo("smart-id.com")); - assertThat(uri.getPath(), equalTo("/dynamic-link/")); - - Map queryParams = toQueryParamsMap(uri); - assertThat(queryParams, hasEntry("version", "0.1")); - assertThat(queryParams, hasEntry("dynamicLinkType", qrCode.getValue())); - assertThat(queryParams, hasEntry("sessionType", sessionType.getValue())); - assertThat(queryParams, hasEntry("sessionToken", "sessionToken")); - assertThat(queryParams, hasEntry("elapsedSeconds", "1")); - assertThat(queryParams, hasEntry(equalTo("authCode"), matchesPattern("^[A-Za-z0-9_-]+={0,2}$"))); - } + @Test + void buildDeviceLink_missingDigestForAuth_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("digest must be set for AUTH or SIGN flows", ex.getMessage()); + } + + @Test + void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInitialCallbackUrl(CALLBACK_URL) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("initialCallbackUrl must be empty for QR_CODE flow", exception.getMessage()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"APP_2_APP", "WEB_2_APP"}) + void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLinkType deviceLinkType) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(deviceLinkType) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("initialCallbackUrl must be provided for same-device flows", exception.getMessage()); + } + + @Test + void buildDeviceLink_withEmptyUnprotectedUri_shouldThrowException() { + var builder = new DeviceLinkBuilder() { + @Override + public URI createUnprotectedUri() { + return URI.create(""); + } + }; + + builder + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withElapsedSeconds(1L) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withLang(LANGUAGE) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(SESSION_SECRET)); + assertEquals("unprotected device-link must not be empty", exception.getMessage()); + } + + @Test + void buildDeviceLink_sameDeviceFlowWithCallback() { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withInitialCallbackUrl(CALLBACK_URL) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + + @Test + void calculateAuthCode_invalidBase64Key_shouldThrowException() { + var builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); + + assertEquals("Failed to calculate authCode", exception.getMessage()); + assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); + } - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); } private static Map toQueryParamsMap(URI uri) { return Arrays.stream(uri.getQuery().split("&")) - .map(param -> param.split("=")) - .collect(Collectors.toMap(param -> param[0], param -> param[1])); + .map(s -> s.split("=")) + .collect(Collectors.toMap(s -> s[0], s -> s[1])); } } diff --git a/src/test/java/ee/sk/smartid/InteractionUtil.java b/src/test/java/ee/sk/smartid/InteractionUtil.java index 9e643aee..0de05015 100644 --- a/src/test/java/ee/sk/smartid/InteractionUtil.java +++ b/src/test/java/ee/sk/smartid/InteractionUtil.java @@ -27,6 +27,8 @@ */ import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.rest.dao.Interaction; + import org.bouncycastle.util.encoders.Base64; import java.nio.charset.StandardCharsets; @@ -39,7 +41,7 @@ public class InteractionUtil { private InteractionUtil() { } - public static String encodeInteractionsAsBase64(List interactions) { + public static String encodeInteractionsAsBase64(List interactions) { try { String json = objectMapper.writeValueAsString(interactions); return Base64.toBase64String(json.getBytes(StandardCharsets.UTF_8)); diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index e15ef21f..4a8f1e18 100644 --- a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -154,13 +154,17 @@ void convertToBase64() { } private static URI createUri() { - return new DeviceLinkBuilder() + var linkBuilder = new DeviceLinkBuilder() + .withDeviceLinkBase("smartid://link") .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") - .withAuthCode("Y7jBVqtP_KcY4GyJ0gTK717wZnfRLvondEUjjCRJAsQ") .withElapsedSeconds(1L) - .createUri(); + .withRelyingPartyName("DEMO") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withLang("ENG"); + + return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); } private static BufferedImage convertToBufferedImage(String qrDataUri) { diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 8d8c214e..60794c89 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -133,7 +133,7 @@ void createDeviceLinkAuthentication_anonymous() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -151,7 +151,7 @@ void createDeviceLinkAuthentication_withDocumentNumber() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -169,7 +169,7 @@ void createDeviceLinkAuthentication_withSemanticsIdentifier() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -346,31 +346,58 @@ void getSessionStatus() { class DynamicContent { @ParameterizedTest - @EnumSource - void createDynamicContent_authenticationWithDifferentDynamicLinkTypes(DeviceLinkType deviceLinkType) { + @EnumSource(value = DeviceLinkType.class, names = { "WEB_2_APP", "APP_2_APP" }) + void createDynamicContent_authenticationWithWeb2AppAndApp2App(DeviceLinkType deviceLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA_512) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .initAuthenticationSession(); - long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(deviceLinkType, SessionType.AUTHENTICATION, elapsedSeconds, response.getSessionSecret()); - URI qrCodeUri = smartIdClient.createDynamicContent() + URI qrCodeUri = new DeviceLinkBuilder() + .withDeviceLinkBase(response.getDeviceLinkBase().toString()) .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(response.getSessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withUserLanguage("eng") - .withAuthCode(authCode) - .createUri(); + .withRelyingPartyName("DEMO") + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInitialCallbackUrl("https://smart-id.com/callback") + .buildDeviceLink(response.getSessionSecret()); assertUri(qrCodeUri, SessionType.AUTHENTICATION, deviceLinkType, response.getSessionToken()); } + @Test + void createDynamicContent_authenticationWithQRCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .initAuthenticationSession(); + + long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + + URI qrCodeUri = new DeviceLinkBuilder() + .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.getSessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withRelyingPartyName("DEMO") + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .buildDeviceLink(response.getSessionSecret()); + + assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.getSessionToken()); + } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") @ParameterizedTest @EnumSource @@ -383,17 +410,18 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceL .initCertificateChoice(); long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(deviceLinkType, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); - URI qrCodeUri = smartIdClient.createDynamicContent() + + URI fullUri = new DeviceLinkBuilder() + .withDeviceLinkBase(response.getDeviceLinkBase().toString()) .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) - .withUserLanguage("eng") - .withAuthCode(authCode) - .createUri(); + .withLang("eng") + .withRelyingPartyName("DEMO") + .buildDeviceLink(response.getSessionSecret()); - assertUri(qrCodeUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); + assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); } @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") @@ -407,18 +435,21 @@ void createDynamicContent_createQrCode() { .initCertificateChoice(); long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.CERTIFICATE_CHOICE, elapsedSeconds, response.getSessionSecret()); - String qrCodeDataUri = smartIdClient.createDynamicContent() + + URI fullUri = new DeviceLinkBuilder() + .withDeviceLinkBase(response.getDeviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) - .withUserLanguage("eng") - .withAuthCode(authCode) - .createQrCodeDataUri(); + .withLang("eng") + .withRelyingPartyName("DEMO") + .buildDeviceLink(response.getSessionSecret()); + String qrCodeDataUri = QrCodeGenerator.generateDataUri(fullUri.toString()); String[] qrCodeDataUriParts = qrCodeDataUri.split(","); URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.getSessionToken()); } @@ -427,11 +458,10 @@ private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLink assertEquals("smart-id.com", qrCodeUri.getHost()); assertEquals("/device-link/", qrCodeUri.getPath()); - assertTrue(qrCodeUri.getQuery().contains("version=0.1")); + assertTrue(qrCodeUri.getQuery().contains("version=1.0")); assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("dynamicLinkType=" + deviceLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); - assertTrue(qrCodeUri.getQuery().contains("elapsedSeconds=")); assertTrue(qrCodeUri.getQuery().contains("lang=eng")); assertTrue(qrCodeUri.getQuery().contains("authCode=")); } diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 38cbb0ae..c34aa115 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,12 +33,14 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.time.Duration; import java.time.Instant; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @@ -52,8 +54,8 @@ import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.HashType; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.AuthCode; import ee.sk.smartid.AuthenticationCertificateLevel; import ee.sk.smartid.AuthenticationResponse; import ee.sk.smartid.AuthenticationResponseMapper; @@ -103,14 +105,15 @@ class DynamicLinkExamples { @Test void anonymousAuthentication_withApp2App() { // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RpChallengeGenerator.generate(); - // Store generated randomChallenge only on backend side. Do not expose it to the client side. + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(randomChallenge) + .withRpChallenge(rpChallenge) + .withHashAlgorithm(HashAlgorithm.SHA3_512) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withInteractions(Collections.singletonList( // before the user can enter PIN. If user selects wrong verification code then the operation will fail. @@ -128,37 +131,34 @@ void anonymousAuthentication_withApp2App() { // Will be used to calculate elapsed time being used in dynamic link and in authCode Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + // Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Generate auth code - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); - // Generate dynamic link - URI dynamicLink = smartIdClient.createDynamicContent() - .withDeviceLinkType(DeviceLinkType.APP_2_APP) // specify the type of dynamic link - .withSessionType(SessionType.AUTHENTICATION) // specify type of the session the dynamic link is for - .withSessionToken(sessionToken) // provide token from sessions response - .withElapsedSeconds(elapsedSeconds) // calculate elapsed seconds from response received time - .withAuthCode(authCode) - .createUri(); - // Return dynamic-link to the frontend to be used by the user. + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); - // Get the session status poller + // Use the sessionId from the authentication session response to poll for session status updates SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get sessionID from current session response and poll for session status SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - + // The session can have different states such as RUNNING or COMPLETE. + // Check that the session has completed successfully assertEquals("COMPLETE", sessionStatus.getState()); - // validate sessions status result and map session status to authentication response + // Map the final session status to an authentication response object AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - // validate certificate value and signature and map it to authentication identity - var authenticationResponseValidator = new AuthenticationResponseValidator(); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator().toAuthenticationIdentity(authenticationResponse, rpChallenge); - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); @@ -175,20 +175,19 @@ void authentication_withSemanticIdentifierAndQrCode() { "40504040001"); // identifier (according to country and identity type reference) // For security reasons a new random challenge must be created for each new authentication request - String randomChallenge = RpChallengeGenerator.generate(); - // Store generated randomChallenge only backend side. Do not expose it to the client side. + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withRpChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option .withShareMdClientIpAddress(true) .initAuthenticationSession(); @@ -200,38 +199,38 @@ void authentication_withSemanticIdentifierAndQrCode() { String sessionSecret = authenticationSessionResponse.getSessionSecret(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + // Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Generate auth code - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); - // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) - String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(sessionToken) // provide token from sessions response + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) - .withAuthCode(authCode) - .createQrCodeDataUri(); - // Display QR-code to the user + .withLang("est") + .buildDeviceLink(sessionSecret); - // Get the session status poller + // Use sessionId to poll for session status updates SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get sessionID from current session response and poll for session status SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. assertEquals("COMPLETED", sessionStatus.getState()); assertEquals("OK", sessionStatus.getResult().getEndResult()); - // validate sessions status result and map session status to authentication response + // Map the final session status to an authentication response object AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - // validate certificate value and signature and map it to authentication identity - var authenticationResponseValidator = new AuthenticationResponseValidator(); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, "randomChallenge"); + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator() + .toAuthenticationIdentity(authenticationResponse, rpChallenge); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); @@ -243,20 +242,19 @@ void authentication_withDocumentNumberAndQrCode() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new random challenge must be created for each new authentication request - String randomChallenge = RpChallengeGenerator.generate(); - // Store generated randomChallenge only backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication session status OK response DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withRpChallenge(randomChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") )) - // we want to get the IP address of the device running Smart-ID app - // for the IP to be returned the service provider (SK) must switch on this option .withShareMdClientIpAddress(true) .initAuthenticationSession(); @@ -268,45 +266,68 @@ void authentication_withDocumentNumberAndQrCode() { String sessionSecret = authenticationSessionResponse.getSessionSecret(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - // Generate QR-code or dynamic link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse - - // Calculate elapsed seconds from response received time + // Generate the base (unprotected) device link URI, which does not yet include the authCode long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Generate auth code - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.AUTHENTICATION, elapsedSeconds, sessionSecret); - // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) - String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.AUTHENTICATION) // specify type of the sessions the dynamic link is for - .withSessionToken(sessionToken) // provide token from sessions response + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) - .withAuthCode(authCode) - .createQrCodeDataUri(); - // Display QR-code to the user + .withLang("est") + .buildDeviceLink(sessionSecret); - // Get the session status poller + // Use sessionId to poll for session status updates SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get sessionID from current session response and poll for session status SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", sessionStatus.getState()); + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETE", sessionStatus.getState()); assertEquals("OK", sessionStatus.getResult().getEndResult()); - System.out.println("Session completed with result: " + sessionStatus.getResult().getEndResult()); - // validate sessions status result and map session status to authentication response + + // Map the final session status to an authentication response object AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - // validate certificate value and signature and map it to authentication identity - var authenticationResponseValidator = new AuthenticationResponseValidator(); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, "randomChallenge"); + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator() + .toAuthenticationIdentity(authenticationResponse, rpChallenge); + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); assertEquals("EE", authenticationIdentity.getCountry()); } + @Test + void authentication_withBrokeredRpName() { + String rpChallenge = RpChallengeGenerator.generate(); + + DeviceLinkSessionResponse response = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber("PNOLT-40504040001-MOCK-Q") + .withRpChallenge(rpChallenge) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Authorize?"))) + .initAuthenticationSession(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.getSessionToken()) + .withDigest(rpChallenge) + .withRelyingPartyName("DEMO") + .withBrokeredRpName("BANK_XYZ") + .withElapsedSeconds(Duration.between(response.getReceivedAt(), Instant.now()).getSeconds()) + .withLang("est") + .buildDeviceLink(response.getSessionSecret()); + + assertNotNull(deviceLink); + } + @Test void signature_withDocumentNumber() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; @@ -357,16 +378,15 @@ void signature_withDocumentNumber() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); - // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) - String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for - .withSessionToken(sessionToken) // provide token from sessions response + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) - .withAuthCode(authCode) - .createQrCodeDataUri(); - // Display QR-code to the user + .withLang("est") + .buildDeviceLink(sessionSecret); // Get the session status poller poller = smartIdClient.getSessionStatusPoller(); @@ -440,15 +460,15 @@ void signature_withSemanticIdentifier() { // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); // Generate auth code - String authCode = AuthCode.createHash(DeviceLinkType.QR_CODE, SessionType.SIGNATURE, elapsedSeconds, sessionSecret); - // Generate dynamic link Data URI (data:image/png;base64,bash64EncodedImageData..) - String qrCodeDataUri = smartIdClient.createDynamicContent() - .withDeviceLinkType(DeviceLinkType.QR_CODE) // using other values than QR will result in an error - .withSessionType(SessionType.SIGNATURE) // specify type of the sessions the dynamic link is for - .withSessionToken(sessionToken) // provide token from sessions response + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) - .withAuthCode(authCode) - .createQrCodeDataUri(); + .withLang("est") + .buildDeviceLink(sessionSecret); // Display QR-code to the user // Get the session status poller @@ -476,14 +496,14 @@ void authentication_withDocumentNumber() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RpChallengeGenerator.generate(); - // Store generated randomChallenge only on backend side. Do not expose it to the client side. + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient .createNotificationAuthentication() .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) + .withRandomChallenge(rpChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withAllowedInteractionsOrder(Collections.singletonList( NotificationInteraction.verificationCodeChoice("Log in?") @@ -512,7 +532,7 @@ void authentication_withDocumentNumber() { // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, rpChallenge); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); @@ -529,14 +549,14 @@ void authentication_withSemanticIdentifier() { "40504040001"); // identifier (according to country and identity type reference) // For security reasons a new hash value must be created for each new authentication request - String randomChallenge = RpChallengeGenerator.generate(); - // Store generated randomChallenge only on backend side. Do not expose it to the client side. + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient .createNotificationAuthentication() .withSemanticsIdentifier(semanticIdentifier) - .withRandomChallenge(randomChallenge) + .withRandomChallenge(rpChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withAllowedInteractionsOrder(Collections.singletonList( NotificationInteraction.verificationCodeChoice("Log in?") @@ -565,7 +585,7 @@ void authentication_withSemanticIdentifier() { // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, randomChallenge); + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, rpChallenge); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 095739f2..d7bc69ff 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -1014,7 +1014,7 @@ private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionReq dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); var algorithmParameters = new SignatureAlgorithmParameters(); - algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA_512); + algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512); signatureProtocolParameters.setSignatureAlgorithmParameters(algorithmParameters); DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Log in?"); diff --git a/src/test/resources/requests/device-link-authentication-session-request.json b/src/test/resources/requests/device-link-authentication-session-request.json index 1928fc03..4af72eb4 100644 --- a/src/test/resources/requests/device-link-authentication-session-request.json +++ b/src/test/resources/requests/device-link-authentication-session-request.json @@ -6,7 +6,7 @@ "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", "signatureAlgorithm": "rsassa-pss", "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA_512" + "hashAlgorithm": "SHA3-512" } }, "certificateLevel": "QUALIFIED", diff --git a/src/test/resources/responses/device-link-authentication-session-response.json b/src/test/resources/responses/device-link-authentication-session-response.json index b55ad982..79fb9fdb 100644 --- a/src/test/resources/responses/device-link-authentication-session-response.json +++ b/src/test/resources/responses/device-link-authentication-session-response.json @@ -2,6 +2,6 @@ "sessionID": "00000000-0000-0000-0000-000000000000", "sessionToken": "sessionToken", "sessionSecret": "c2Vzc2lvblNlY3JldA==", - "deviceLinkBase": "https://example.org/device-link/", + "deviceLinkBase": "https://smart-id.com/device-link/", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file From 606f63bc7a3f6888e09d8e759d19e7f970c4fb5d Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:22:05 +0300 Subject: [PATCH 23/57] SLIB-94 - Update generating of dynamic content (#113) * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-94 - updated generating of dynamic content * SLIB-94 - refactored device link generation * SLIB-94 - updated javadoc, exceptions * SLIB-94 - refactored, updated README * SLIB-94 - updated README, Javadocs * SLIB-94 - README and tests fixed * SLIB-94 - made schemeName configurable, * SLIB-94 - added shemeName nullcheck, test, updated README * SLIB-94 - moved shemeName nullcheck/test to correct place --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 2 +- README.md | 5 +++ .../java/ee/sk/smartid/DeviceLinkBuilder.java | 34 +++++++++++----- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 40 ++++++++++++++++++- .../java/ee/sk/smartid/SmartIdClientTest.java | 2 + 5 files changed, 70 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bb1153..a1c7617a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Moved `deviceLinkBase` to required input (no more default). - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. - New payload structure includes required and optional fields as per documentation. - - `schemeName` is fixed to `"smart-id"`. + - `schemeName` is now configurable (default is `"smart-id"`). - Does not store `sessionSecret`, ensures it must be passed to the build method. - Removed deprecated dynamic link and QR code generation logic from old builders and helpers. diff --git a/README.md b/README.md index 18f808e4..3006f121 100644 --- a/README.md +++ b/README.md @@ -543,12 +543,16 @@ builder.withAllowedInteractionsOrder(List.of( Documentation to device link and QR-code requirements https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html +To use the Smart-ID **demo environment**, you must specify `smart-id-demo` as `schemeName`. +See: https://sk-eid.github.io/smart-id-documentation/environments.html#_demo + #### Generating device link Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. ##### Device link parameters +* `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. * `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. * `version`: Version of the dynamic link. Only allowed value is `"1.0"`. * `deviceLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. @@ -583,6 +587,7 @@ URI deviceLink = new DeviceLinkBuilder() DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() + .withSchemeName("smart-id-demo") // override default scheme name to use demo environment .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) .withDeviceLinkType(DeviceLinkType.APP_2_APP) .withSessionType(SessionType.AUTHENTICATION) diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 518251f0..01e3a71e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -43,8 +43,8 @@ public class DeviceLinkBuilder { private static final String ALLOWED_VERSION = "1.0"; - private static final String SCHEME_NAME = "smart-id"; + private String schemeName = "smart-id"; private String deviceLinkBase; private String version = ALLOWED_VERSION; private DeviceLinkType deviceLinkType; @@ -59,6 +59,19 @@ public class DeviceLinkBuilder { private String interactions; private String initialCallbackUrl; + /** + * Sets the scheme name for the device link. + *

    + * Default is `smart-id`. + * + * @param schemeName the scheme name to be used in the device link + * @return this builder + */ + public DeviceLinkBuilder withSchemeName(String schemeName) { + this.schemeName = schemeName; + return this; + } + /** * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. *

    @@ -164,7 +177,7 @@ public DeviceLinkBuilder withDigest(String digest) { * @return this builder */ public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyNameBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); + this.relyingPartyNameBase64 = Base64.getEncoder().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); return this; } @@ -176,7 +189,7 @@ public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { * @return this builder */ public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { - this.brokeredRpNameBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); + this.brokeredRpNameBase64 = Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); return this; } @@ -214,14 +227,10 @@ public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { */ public URI createUnprotectedUri() { validateInputParameters(); - UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase) - .queryParam("version", version) - .queryParam("sessionToken", sessionToken) - .queryParam("deviceLinkType", deviceLinkType.getValue()) - .queryParam("sessionType", sessionType.getValue()) - .queryParam("lang", lang); - + UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); addElapsedSecondsIfQrCode(uriBuilder); + uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) + .queryParam("version", version).queryParam("lang", lang); return uriBuilder.build(); } @@ -255,7 +264,7 @@ private String generateAuthCode(String unprotectedLink, String sessionSecret) { private String buildPayload(String unprotectedLink) { return String.join("|", - SCHEME_NAME, + schemeName, getSignatureProtocolForSession(), StringUtil.orEmpty(digest), relyingPartyNameBase64, @@ -313,6 +322,9 @@ private void validateInputParameters() { } private void validateAuthCodeParams(String unprotectedLink) { + if (StringUtil.isEmpty(schemeName)) { + throw new SmartIdClientException("Parameter schemeName must be set"); + } if (StringUtil.isEmpty(relyingPartyNameBase64)) { throw new SmartIdClientException("Parameter relyingPartyName must be set"); } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index adadc572..a302a49a 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -50,6 +50,7 @@ class DeviceLinkBuilderTest { private static final String SESSION_SECRET = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + private static final String DEMO_SCHEMA_NAME = "smart-id-demo"; private static final String DEVICE_LINK_BASE = "https://smart-id.com/device-link/"; private static final String DEVICE_LINK_HOST = "smart-id.com"; private static final String SESSION_TOKEN = "token123"; @@ -268,6 +269,44 @@ void buildDeviceLink(SessionType sessionType) { assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); } + @Test + void buildDeviceLink_withCustomSchemeName() { + String authCode = toQueryParamsMap( + new DeviceLinkBuilder() + .withSchemeName(DEMO_SCHEMA_NAME) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ).get("authCode"); + + assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingSchemeName_throwsException(String scheme) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withSchemeName(scheme) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter schemeName must be set", ex.getMessage()); + } + @Test void buildDeviceLink_missingRelyingPartyName_throwsException() { var ex = assertThrows(SmartIdClientException.class, () -> @@ -406,7 +445,6 @@ void calculateAuthCode_invalidBase64Key_shouldThrowException() { assertEquals("Failed to calculate authCode", exception.getMessage()); assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); } - } private static Map toQueryParamsMap(URI uri) { diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 60794c89..d3fc06ac 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -358,6 +358,7 @@ void createDynamicContent_authenticationWithWeb2AppAndApp2App(DeviceLinkType dev .initAuthenticationSession(); URI qrCodeUri = new DeviceLinkBuilder() + .withSchemeName("smart-id-demo") .withDeviceLinkBase(response.getDeviceLinkBase().toString()) .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.AUTHENTICATION) @@ -385,6 +386,7 @@ void createDynamicContent_authenticationWithQRCode() { long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); URI qrCodeUri = new DeviceLinkBuilder() + .withSchemeName("smart-id-demo") .withDeviceLinkBase(response.getDeviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) From 2b1eb697b55a2ba1d9dc9334fd841e8dbe6e649e Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 2 Jul 2025 07:07:05 +0300 Subject: [PATCH 24/57] SLIB-98 - Add new certificate choice endpoint (#112) * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-93 - refactored, added tests, improved javadoc-s * SLIB-93 - refactored, fixed tests, updated README * SLIB-93 - interactions refacto, fixed javadocs, README * SLIB-93 - renamed withSignatureAlgorithmParameters method to withHashAlgorithm * SLIB-98 - added new certificate choice endpoint * SLIB-98 - reverted AuthenticationResponseValidator changes * SLIB-98 - refactored, fixed javadocs, README * SLIB-98 - README fixes * SLIB-98 - added certificateLevel validation, refactored README * SLIB-98 - refactored state error handling * SLIB-98 - refactored certificate querying logic * SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding * Revert "SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding" This reverts commit f4356ba8603872fd35013cb569e604357c932a8e. * SLIB-98 - refactored, code review fixes * SLIB-98 - corrected unknown json fail * SLIB-98 - fixed naming --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 10 + README.md | 58 +++-- ...ificateByDocumentNumberRequestBuilder.java | 198 ++++++++++++++ .../CertificateByDocumentNumberResult.java | 32 +++ .../java/ee/sk/smartid/CertificateState.java | 33 +++ ...nkAuthenticationSessionRequestBuilder.java | 2 +- ...ertificateChoiceSessionRequestBuilder.java | 24 +- .../java/ee/sk/smartid/SmartIdClient.java | 11 + .../ee/sk/smartid/rest/SmartIdConnector.java | 10 +- .../sk/smartid/rest/SmartIdRestConnector.java | 25 +- .../dao/AuthenticationSessionRequest.java | 4 +- .../CertificateByDocumentNumberRequest.java | 60 +++++ .../sk/smartid/rest/dao/CertificateInfo.java | 54 ++++ .../smartid/rest/dao/CertificateResponse.java | 54 ++++ .../dao/SignatureAlgorithmParameters.java | 6 +- .../AuthenticationResponseValidatorTest.java | 2 +- ...ateByDocumentNumberRequestBuilderTest.java | 241 ++++++++++++++++++ ...thenticationSessionRequestBuilderTest.java | 2 +- ...thenticationSessionRequestBuilderTest.java | 2 +- ...ficateChoiceSessionRequestBuilderTest.java | 22 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 51 ++-- .../integration/ReadmeIntegrationTest.java | 206 ++------------- .../SmartIdRestIntegrationTest.java | 11 - .../rest/SmartIdRestConnectorTest.java | 55 ++-- ...ertificate-by-document-number-request.json | 5 + ...ocument-number-response-unknown-state.json | 9 + ...rtificate-by-document-number-response.json | 9 + 27 files changed, 878 insertions(+), 318 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java create mode 100644 src/main/java/ee/sk/smartid/CertificateState.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java create mode 100644 src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java create mode 100644 src/test/resources/requests/certificate-by-document-number-request.json create mode 100644 src/test/resources/responses/certificate-by-document-number-response-unknown-state.json create mode 100644 src/test/resources/responses/certificate-by-document-number-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c7617a..d6574afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.3] - 2025-06-13 + +### Added + +- Added new endpoint: `POST /v3/signature/certificate/{document-number}`. + +### Removed + +- Removed notification-based certificate choice request with document number. + ## [3.1.2] - 2025-06-05 ### Changed diff --git a/README.md b/README.md index 3006f121..9a37e564 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This library supports Smart-ID API v3.1. * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) * [Example of validating the signature](#example-of-validating-the-signature-session-response) * [Error handling for session status](#error-handling-for-session-status) + * [Certificate by document number](#certificate-by-document-number) * [Notification-based flows](#notification-based-flows) * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-dynamic-link-flows) * [Notification-based authentication session](#notification-based-authentication-session) @@ -62,7 +63,6 @@ This library supports Smart-ID API v3.1. * [Notification-based certificate choice session](#notification-based-certificate-choice-session) * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) - * [Initiating notification certificate choice with document number](#initiating-a-notification-based-authentication-session-with-document-number) * [Notification-based signature session](#notification-based-signature-session) * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) @@ -822,6 +822,46 @@ The session status response may return various error codes indicating the outcom * `USER_REFUSED_CONFIRMATIONMESSAGE`: User cancelled on confirmationMessage screen. * `USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE`: User cancelled on confirmationMessageAndVerificationCodeChoice screen. +## Certificate by document number + +In API v3.1, the flow to initiate a **notification-based certificate choice session using a document number** was removed. Instead, a new, simplified endpoint was introduced. + +### Request Parameters +The request parameters for the certificate by document number request are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are `ADVANCED`, `QUALIFIED` or `QSCD`. Defaults to `QUALIFIED`. + +### Response Parameters +* `state`: Required. Indicates result. Possible values: + * `OK`: Certificate found and returned. + * `DOCUMENT_UNUSABLE`: user's Smart-ID account is not usable for signing +* `cert`: Required. Object containing the signing certificate. + * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) + * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` + +### Get certificate using document number + +RP can directly query the user's signing certificate by document number — no session flow or user interaction required. + +#### Usage example in Java + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +CertificateByDocumentNumberResult certResponse = client + .createCertificateByDocumentNumber + .withDocumentNumber(documentNumber) + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .getCertificateByDocumentNumber(); + +// certResponse.getCertificate(); contains Base64-encoded certificate +// certResponse.getCertificateLevel(); is either ADVANCED or QUALIFIED +``` + ## Notification-based flows ### Differences between notification-based and dynamic-link flows @@ -962,22 +1002,6 @@ String sessionId = certificateChoiceSessionResponse.getSessionID(); ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -##### Initiating a notification-based certificate choice session using document number - -```java -String documentNumber = "PNOLT-30303039914-MOCK-Q"; - -NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - -String sessionId = certificateChoiceSessionResponse.getSessionID(); -// SessionID is used to query sessions status later -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - ### Notification-based signature session #### Request Parameters diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java new file mode 100644 index 00000000..0af5bfb6 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -0,0 +1,198 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.util.StringUtil.isEmpty; + +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.util.StringUtil; + +public class CertificateByDocumentNumberRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); + + private final SmartIdConnector connector; + + private String documentNumber; + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel = CertificateLevel.QUALIFIED; + + /** + * Constructs a new CertificateByDocumentNumberRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public CertificateByDocumentNumberRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the document number for the request. + * + * @param documentNumber the document number + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the relying party UUID for the request. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name for the request. + * + * @param relyingPartyName the relying party name + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level for the request. + * + * @param certificateLevel the certificate level + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Builds the request and retrieves the certificate by document number. + * + * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate + * @throws SmartIdClientException if any required parameters are missing or invalid + * @throws UnprocessableSmartIdResponseException if the response is not valid + * @throws DocumentUnusableException if the document is unusable + */ + public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { + validateRequestParameters(); + var request = new CertificateByDocumentNumberRequest(); + request.setRelyingPartyUUID(relyingPartyUUID); + request.setRelyingPartyName(relyingPartyName); + request.setCertificateLevel(certificateLevel.name()); + CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); + validateResponseParameters(response); + + return new CertificateByDocumentNumberResult(CertificateLevel.valueOf(response.getCert().getCertificateLevel()), CertificateParser.parseX509Certificate(response.getCert().getValue())); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(documentNumber)) { + logger.error("Parameter documentNumber must be set"); + throw new SmartIdClientException("Parameter documentNumber must be set"); + } + if (StringUtil.isEmpty(relyingPartyUUID)) { + logger.error("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + logger.error("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter relyingPartyName must be set"); + } + } + + private void validateResponseParameters(CertificateResponse certificateResponse) { + if (certificateResponse == null) { + logger.error("CertificateByDocumentNumberResponse is null"); + throw new UnprocessableSmartIdResponseException("Certificate certificateByDocumentNumberResponse is null"); + } + handleResponseState(certificateResponse.getState()); + validateCertificateLevel(certificateResponse.getCert().getCertificateLevel()); + + if (certificateResponse.getCert() == null || isEmpty(certificateResponse.getCert().getValue())) { + logger.error("Parameter cert.value is missing"); + throw new UnprocessableSmartIdResponseException("Parameter cert.value is missing"); + } + + if (!BASE64_PATTERN.matcher(certificateResponse.getCert().getValue()).matches()) { + logger.error("Parameter cert.value is not a valid Base64-encoded string"); + throw new UnprocessableSmartIdResponseException("Parameter cert.value is not a valid Base64-encoded string"); + } + } + + private void validateCertificateLevel(String certificateLevel) { + if (StringUtil.isEmpty(certificateLevel)) { + logger.error("Parameter certificateLevel is missing"); + throw new UnprocessableSmartIdResponseException("Parameter certificateLevel is missing"); + } + + try { + CertificateLevel level = CertificateLevel.valueOf(certificateLevel); + if (!level.isSameLevelOrHigher(this.certificateLevel)) { + logger.error("Certificate level is lower than requested"); + throw new UnprocessableSmartIdResponseException("Certificate level is lower than requested"); + } + } catch (IllegalArgumentException e) { + logger.error("Invalid certificateLevel: {}", certificateLevel); + throw new UnprocessableSmartIdResponseException("Invalid certificateLevel: " + certificateLevel); + } + } + + private void handleResponseState(String state) { + if (isEmpty(state)) { + logger.error("Response state is missing"); + throw new UnprocessableSmartIdResponseException("Missing response 'state'"); + } + + try { + if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { + logger.error("Document is unusable"); + throw new DocumentUnusableException(); + } + } catch (IllegalArgumentException e) { + logger.error("Unsupported certificate state: {}", state); + throw new UnprocessableSmartIdResponseException("Unsupported certificate state: " + state); + } + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java new file mode 100644 index 00000000..7bc829d4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java @@ -0,0 +1,32 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { +} diff --git a/src/main/java/ee/sk/smartid/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java new file mode 100644 index 00000000..b064b2ef --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -0,0 +1,33 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public enum CertificateState { + OK, + DOCUMENT_UNUSABLE +} + diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index b0c162c2..353f312a 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -331,7 +331,7 @@ private AuthenticationSessionRequest createAuthenticationRequest() { signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); var signatureAlgorithmParameters = new SignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(this.hashAlgorithm); + signatureAlgorithmParameters.setHashAlgorithm(this.hashAlgorithm.getValue()); signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); request.setSignatureProtocolParameters(signatureProtocolParameters); diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 09b097be..2b82bd7f 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -51,7 +51,6 @@ public class NotificationCertificateChoiceSessionRequestBuilder { private String nonce; private Set capabilities; private Boolean shareMdClientIpAddress; - private String documentNumber; private SemanticsIdentifier semanticsIdentifier; /** @@ -129,19 +128,6 @@ public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAdd return this; } - /** - * Sets the document number - *

    - * Setting this value will make the notification session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - /** * Sets the semantics identifier *

    @@ -161,7 +147,6 @@ public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifie * There are 2 supported ways to start authentication session: *

      *
    • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
    • - *
    • with document number by using {@link #withDocumentNumber(String)}
    • *
    * * @return init session response @@ -175,13 +160,10 @@ public NotificationCertificateChoiceSessionResponse initCertificateChoice() { } private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(CertificateChoiceSessionRequest request) { - if (semanticsIdentifier != null) { - return connector.initNotificationCertificateChoice(request, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initNotificationCertificateChoice(request, documentNumber); - } else { - throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set."); + if (semanticsIdentifier == null) { + throw new SmartIdClientException("SemanticsIdentifier must be set."); } + return connector.initNotificationCertificateChoice(request, semanticsIdentifier); } private void validateRequestParameters() { diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 185c5cec..a0f12198 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -122,6 +122,17 @@ public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { .withRelyingPartyName(relyingPartyName); } + /** + * Creates a new builder for requesting a certificate using document number. + * + * @return builder for querying certificate using document number + */ + public CertificateByDocumentNumberRequestBuilder createCertificateByDocumentNumber() { + return new CertificateByDocumentNumberRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + /** * Creates a new builder for creating a new notification signature session request * diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 0160718b..aea29979 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -32,6 +32,8 @@ import javax.net.ssl.SSLContext; import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; @@ -79,13 +81,13 @@ public interface SmartIdConnector extends Serializable { NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** - * Initiates a notification based certificate choice request. + * Queries signing certificate by document number. * - * @param request CertificateChoiceSessionRequest containing necessary parameters + * @param request CertificateByDocumentNumberRequest containing necessary parameters * @param documentNumber The document number - * @return NotificationCertificateChoiceSessionResponse containing sessionID + * @return CertificateResponse containing response state and certificate information. */ - NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, String documentNumber); + CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); /** * Initiates a dynamic link based signature sessions. diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 3461d48d..464a055b 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -44,6 +44,8 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; @@ -78,7 +80,8 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String SESSION_STATUS_URI = "/session/{sessionId}"; private static final String CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH = "/certificatechoice/dynamic-link/anonymous"; private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; - private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_DOCUMENT_NUMBER_PATH = "/certificatechoice/notification/document"; + + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; private static final String DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/dynamic-link/etsi"; private static final String DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/dynamic-link/document"; @@ -93,6 +96,7 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; + private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; @@ -197,14 +201,13 @@ public NotificationCertificateChoiceSessionResponse initNotificationCertificateC return postNotificationCertificateChoiceRequest(uri, request); } - @Override - public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, String documentNumber) { + public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { URI uri = UriBuilder .fromUri(endpointUrl) - .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_DOCUMENT_NUMBER_PATH) + .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postNotificationCertificateChoiceRequest(uri, request); + return postCertificateByDocumentNumberRequest(uri, request); } @Override @@ -344,6 +347,18 @@ private NotificationCertificateChoiceSessionResponse postNotificationCertificate } } + private CertificateResponse postCertificateByDocumentNumberRequest(URI uri, CertificateByDocumentNumberRequest request) { + try { + return postRequest(uri, request, CertificateResponse.class); + } catch (NotFoundException ex) { + logger.warn("User account not found for URI {}", uri, ex); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } + } + private DeviceLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { return postRequest(uri, request, DeviceLinkSessionResponse.class); diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java index 2c2caaf5..1b4be422 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -41,7 +41,7 @@ public class AuthenticationSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String certificateLevel; - private final SignatureProtocol signatureProtocol = SignatureProtocol.ACSP_V2; + private final String signatureProtocol = SignatureProtocol.ACSP_V2.name(); private AcspV2SignatureProtocolParameters acspV2SignatureProtocolParameters; @@ -80,7 +80,7 @@ public void setCertificateLevel(String certificateLevel) { this.certificateLevel = certificateLevel; } - public SignatureProtocol getSignatureProtocol() { + public String getSignatureProtocol() { return signatureProtocol; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java new file mode 100644 index 00000000..3028199c --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java @@ -0,0 +1,60 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +public class CertificateByDocumentNumberRequest implements Serializable { + + private String relyingPartyUUID; + private String relyingPartyName; + private String certificateLevel; + + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + public String getRelyingPartyName() { + return relyingPartyName; + } + + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java new file mode 100644 index 00000000..ef80a451 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CertificateInfo implements Serializable { + + private String value; + private String certificateLevel; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getCertificateLevel() { + return certificateLevel; + } + + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java new file mode 100644 index 00000000..c758f174 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CertificateResponse implements Serializable { + + private String state; + private CertificateInfo cert; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public CertificateInfo getCert() { + return cert; + } + + public void setCert(CertificateInfo cert) { + this.cert = cert; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index 4f8e1974..e1a06daa 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -30,13 +30,13 @@ public class SignatureAlgorithmParameters implements Serializable { - private HashAlgorithm hashAlgorithm; + private String hashAlgorithm; - public HashAlgorithm getHashAlgorithm() { + public String getHashAlgorithm() { return hashAlgorithm; } - public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { + public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 75581e9a..fb4e22be 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -314,4 +314,4 @@ private X509Certificate toX509Certificate(String certificate) { private static String toBase64(String data) { return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); } -} +} \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java new file mode 100644 index 00000000..843051ea --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java @@ -0,0 +1,241 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateInfo; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; + +class CertificateByDocumentNumberRequestBuilderTest { + + private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String RP_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RP_NAME = "DEMO"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initCertificate_ByDocumentNumber_ok() { + CertificateResponse mockResponse = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + var result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.getRelyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.getRelyingPartyName()); + assertEquals("QUALIFIED", sentRequest.getCertificateLevel()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void getCertificate_documentNumberMissing_throwException(String documentNumber) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withDocumentNumber(documentNumber); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter documentNumber must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificate_relyingPartyUUIDMissing_throwException(String uuid) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyName(RP_NAME) + .withRelyingPartyUUID(uuid); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificate_relyingPartyNameMissing_throwException(String relyingPartyName) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(relyingPartyName); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @Test + void getCertificate_certValueMissing_throwException() { + CertificateResponse response = createValidResponse(null, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter cert.value is missing", ex.getMessage()); + } + + @Test + void getCertificate_certValueInvalidBase64_throwException() { + var cert = new CertificateInfo(); + cert.setValue("NOT@BASE64!"); + cert.setCertificateLevel(CertificateLevel.QUALIFIED.name()); + + var response = new CertificateResponse(); + response.setCert(cert); + response.setState(CertificateState.OK.name()); + + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter cert.value is not a valid Base64-encoded string", ex.getMessage()); + } + + @Test + void getCertificate_responseStateIsDocumentUnusable_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + response.setState(CertificateState.DOCUMENT_UNUSABLE.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); + } + + @Test + void getCertificate_responseIsNull_throwException() { + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Certificate certificateByDocumentNumberResponse is null", ex.getMessage()); + } + + @Test + void getCertificate_responseStateMissing_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + response.setState(null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Missing response 'state'", ex.getMessage()); + } + + @Test + void getCertificate_responseCertificateLevelMissing_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Parameter certificateLevel is missing", ex.getMessage()); + } + + @Test + void getCertificate_certificateLevelLowerThanRequested_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Certificate level is lower than requested", ex.getMessage()); + } + } + + private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() { + return new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME); + } + + private CertificateResponse createValidResponse(String certValue, String level) { + var certificate = new CertificateInfo(); + certificate.setValue(certValue); + certificate.setCertificateLevel(level); + + var response = new CertificateResponse(); + response.setCert(certificate); + response.setState(CertificateState.OK.name()); + return response; + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 374e2572..65e35609 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -102,7 +102,7 @@ void initAuthenticationSession_ok() throws Exception { assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); assertEquals("DEMO", request.getRelyingPartyName()); assertEquals("QUALIFIED", request.getCertificateLevel()); - assertEquals(SignatureProtocol.ACSP_V2, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.getSignatureProtocol()); assertNotNull(request.getSignatureProtocolParameters()); assertNotNull(request.getSignatureProtocolParameters().getRpChallenge()); assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index e6af731a..ef58e2ce 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -94,7 +94,7 @@ void initAuthenticationSession_ok() { assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); assertEquals("DEMO", request.getRelyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.getSignatureProtocol()); assertNotNull(request.getSignatureProtocolParameters()); assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); assertNotNull(request.getInteractions()); diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 2bd05296..fd43ab26 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -91,26 +91,6 @@ void initCertificateChoiceSession_withSemanticsIdentifier() { assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); } - @Test - void initCertificateChoiceSession_withDocumentNumber() { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(String.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .initCertificateChoice(); - - ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), documentNumberCaptor.capture()); - String capturedDocumentNumber = documentNumberCaptor.getValue(); - - assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); - } - @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { @@ -260,7 +240,7 @@ void initCertificateChoiceSession_semanticsIdentifierOrDocumentNumberMissing_thr .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.QUALIFIED) .initCertificateChoice()); - assertEquals("Either documentNumber or semanticsIdentifier must be set.", exception.getMessage()); + assertEquals("SemanticsIdentifier must be set.", exception.getMessage()); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index d3fc06ac..5362f04b 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; @@ -45,6 +46,7 @@ import org.junit.jupiter.params.provider.EnumSource; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; @@ -108,19 +110,6 @@ void createNotificationCertificateChoice_withSemanticsIdentifier() { assertNotNull(response.getSessionID()); } - - @Test - void createNotificationCertificateChoice_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/document/PNOEE-1234567890-MOCK-Q", "requests/certificate-choice-session-request.json", "responses/notification-certificate-choice-session-response.json"); - - NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initCertificateChoice(); - - assertNotNull(response.getSessionID()); - } } @Nested @@ -229,6 +218,38 @@ void createDynamicLinkSignature_withSemanticsIdentifier() { } } + @Nested + @WireMockTest(httpPort = 18089) + class CertificateByDocumentNumberRequest { + + @Test + void createCertificateRequest_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response.json"); + + CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withCertificateLevel(CertificateLevel.ADVANCED) + .getCertificateByDocumentNumber(); + + assertNotNull(response); + assertEquals(CertificateLevel.QUALIFIED, response.certificateLevel()); + assertNotNull(response.certificate()); + } + + @Test + void getCertificateByDocumentNumber_withUnknownState_throwsException() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response-unknown-state.json"); + + CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withCertificateLevel(CertificateLevel.ADVANCED); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + + assertTrue(ex.getMessage().contains("Unsupported certificate state")); + } + } + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18089) @@ -400,7 +421,7 @@ void createDynamicContent_authenticationWithQRCode() { assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.getSessionToken()); } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") @ParameterizedTest @EnumSource void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceLinkType deviceLinkType) { @@ -426,7 +447,7 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceL assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") @Test void createDynamicContent_createQrCode() { SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index c34aa115..7f9f4d12 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -300,115 +300,18 @@ void authentication_withDocumentNumberAndQrCode() { assertEquals("EE", authenticationIdentity.getCountry()); } - @Test - void authentication_withBrokeredRpName() { - String rpChallenge = RpChallengeGenerator.generate(); - - DeviceLinkSessionResponse response = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber("PNOLT-40504040001-MOCK-Q") - .withRpChallenge(rpChallenge) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Authorize?"))) - .initAuthenticationSession(); - - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.getSessionToken()) - .withDigest(rpChallenge) - .withRelyingPartyName("DEMO") - .withBrokeredRpName("BANK_XYZ") - .withElapsedSeconds(Duration.between(response.getReceivedAt(), Instant.now()).getSeconds()) - .withLang("est") - .buildDeviceLink(response.getSessionSecret()); - - assertNotNull(deviceLink); - } - - @Test - void signature_withDocumentNumber() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); - - // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA512); - - // Build the dynamic link signature request - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .initSignatureSession(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.getSessionID(); - String sessionToken = signatureSessionResponse.getSessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.getSessionSecret(); - Instant receivedAt = signatureSessionResponse.getReceivedAt(); - - // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - - // Get the session status poller - poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - @Test void signature_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient .createNotificationCertificateChoice() - .withDocumentNumber("PNOLT-40504040001-MOCK-Q") + .withSemanticsIdentifier(semanticIdentifier) .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); @@ -592,32 +495,6 @@ void authentication_withSemanticIdentifier() { assertEquals("LT", authenticationIdentity.getCountry()); } - @Test - void certificateChoice_withDocumentNumber() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; // returned in authentication result and used for re-authentication - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String sessionId = certificateChoiceSessionResponse.getSessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus sessionStatus = poller.getSessionStatus(sessionId); - CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); - assertNotNull(response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - @Test void certificateChoice_withSemanticIdentifier() { var semanticsIdentifier = new SemanticsIdentifier( @@ -649,69 +526,18 @@ void certificateChoice_withSemanticIdentifier() { assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); } - @Test - void signature_withDocumentNumber(){ - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withDocumentNumber(documentNumber) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); - // For example use SignatureBuilder from digidoc4j to create DataToSign using certificateChoiceResponse.getCertificate(); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA512); - - NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - NotificationInteraction.verificationCodeChoice("Please sign the document")) - ) - .initSignatureSession(); - - // Process the querying sessions status response - String sessionID = signatureSessionResponse.getSessionID(); - - // Display verification code to the user - String verificationCode = signatureSessionResponse.getVc().getValue(); - assertTrue(Pattern.matches(ALPHA_NUMERIC_PATTERN, verificationCode)); - - // Get sessionID from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); - assertEquals("verificationCodeChoice", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - @Test void signature_withSemanticsIdentifier(){ + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient .createNotificationCertificateChoice() - .withDocumentNumber("PNOEE-40504040001-MOCK-Q") + .withSemanticsIdentifier(semanticIdentifier) .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 7787bd96..49fb0127 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -266,17 +266,6 @@ void initNotificationCertificateChoice_withSemanticIdentifier() { assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); } - - @Test - void initNotificationCertificateChoice_withDocumentNumber() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - - NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, "PNOEE-40504040001-MOCK-Q"); - - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - } } @Nested diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index d7bc69ff..0d3eecdd 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -64,7 +64,9 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.HashAlgorithm; @@ -459,7 +461,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-98") + @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") @Nested @WireMockTest(httpPort = 18089) class DynamicLinkCertificateChoiceTests { @@ -617,46 +619,41 @@ void initCertificateChoice_requestIsUnauthorized_throwException() { } @Nested - @WireMockTest(httpPort = 18089) - class DocumentNumberNotificationCertificateChoiceTests { + @WireMockTest(httpPort = 18086) + class CertificateByDocumentNumberTests { - private static final String CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH = "/certificatechoice/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; private SmartIdRestConnector connector; @BeforeEach - public void setUp() { - WireMock.configureFor("localhost", 18089); - connector = new SmartIdRestConnector("http://localhost:18089"); + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18086"); } @Test - void initCertificateChoice_withDocumentNumber_successful() { - stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "responses/notification-certificate-choice-session-response.json"); + void getCertificateByDocumentNumber_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response.json"); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - - NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, documentNumber); + CertificateResponse response = connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + assertEquals("OK", response.getState()); + assertNotNull(response.getCert()); + assertEquals("QUALIFIED", response.getCert().getCertificateLevel()); + assertThat(response.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); } @Test - void initNotificationAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "requests/certificate-choice-session-request.json"); - connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); - }); + void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); + assertThrows(UserAccountNotFoundException.class, () -> {connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest());}); } @Test - void initNotificationAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_DOCUMENT_NR_PATH, "requests/certificate-choice-session-request.json"); - connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), "PNOEE-48010010101-MOCK-Q"); - }); + void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> {connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest());}); } } @@ -1014,7 +1011,7 @@ private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionReq dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); var algorithmParameters = new SignatureAlgorithmParameters(); - algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512); + algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getValue()); signatureProtocolParameters.setSignatureAlgorithmParameters(algorithmParameters); DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Log in?"); @@ -1048,6 +1045,14 @@ private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest return request; } + private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { + var request = new CertificateByDocumentNumberRequest(); + request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + request.setRelyingPartyName("DEMO"); + request.setCertificateLevel("ADVANCED"); + return request; + } + private static SignatureSessionRequest createSignatureSessionRequest() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); diff --git a/src/test/resources/requests/certificate-by-document-number-request.json b/src/test/resources/requests/certificate-by-document-number-request.json new file mode 100644 index 00000000..88c2a4d0 --- /dev/null +++ b/src/test/resources/requests/certificate-by-document-number-request.json @@ -0,0 +1,5 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" +} \ No newline at end of file diff --git a/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json new file mode 100644 index 00000000..5491eb3e --- /dev/null +++ b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json @@ -0,0 +1,9 @@ +{ + "state": "UNKNOWN", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/certificate-by-document-number-response.json b/src/test/resources/responses/certificate-by-document-number-response.json new file mode 100644 index 00000000..78e18794 --- /dev/null +++ b/src/test/resources/responses/certificate-by-document-number-response.json @@ -0,0 +1,9 @@ +{ + "state": "OK", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file From c78345c2c30c6d1816553e981052d4509ea481e3 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:08:45 +0300 Subject: [PATCH 25/57] SLIB-105 - Replaced dynamic link signature flows with device link signature (#115) * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-93 - refactored, added tests, improved javadoc-s * SLIB-93 - refactored, fixed tests, updated README * SLIB-93 - interactions refacto, fixed javadocs, README * SLIB-93 - renamed withSignatureAlgorithmParameters method to withHashAlgorithm * SLIB-98 - added new certificate choice endpoint * SLIB-98 - reverted AuthenticationResponseValidator changes * SLIB-98 - refactored, fixed javadocs, README * SLIB-98 - README fixes * SLIB-98 - added certificateLevel validation, refactored README * SLIB-98 - refactored state error handling * SLIB-98 - refactored certificate querying logic * SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding * Revert "SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding" This reverts commit f4356ba8603872fd35013cb569e604357c932a8e. * SLIB-98 - refactored, code review fixes * SLIB-98 - corrected unknown json fail * SLIB-98 - fixed naming * SLIB-105 - replaced dynamic-link signature flows with device-link signature * SLIB-105 - added missing initialCallbackURL to request * SLIB-105 - removed withCertificateChoice, fixed tests, README --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 10 + README.md | 58 +++--- ...ceLinkSignatureSessionRequestBuilder.java} | 128 +++++++----- ...icationSignatureSessionRequestBuilder.java | 4 +- .../java/ee/sk/smartid/SmartIdClient.java | 8 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 12 +- .../sk/smartid/rest/SmartIdRestConnector.java | 19 +- .../RawDigestSignatureProtocolParameters.java | 9 + .../rest/dao/SignatureSessionRequest.java | 26 ++- .../NotificationUtil.java} | 30 +-- ...nkSignatureSessionRequestBuilderTest.java} | 188 +++++++++++------- .../java/ee/sk/smartid/SmartIdClientTest.java | 30 +-- .../integration/ReadmeIntegrationTest.java | 4 +- .../SmartIdRestIntegrationTest.java | 14 +- .../rest/SmartIdRestConnectorTest.java | 64 +++--- .../device-link-signature-request.json | 12 +- ...vice-link-signature-session-response.json} | 1 + 17 files changed, 375 insertions(+), 242 deletions(-) rename src/main/java/ee/sk/smartid/{DynamicLinkSignatureSessionRequestBuilder.java => DeviceLinkSignatureSessionRequestBuilder.java} (63%) rename src/main/java/ee/sk/smartid/{SignatureAlgorithmParameters.java => util/NotificationUtil.java} (62%) rename src/test/java/ee/sk/smartid/{DynamicLinkSignatureSessionRequestBuilderTest.java => DeviceLinkSignatureSessionRequestBuilderTest.java} (64%) rename src/test/resources/responses/{dynamic-link-signature-session-response.json => device-link-signature-session-response.json} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6574afd..a3012c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.5] - 2025-06-30 + +- Renamed dynamic-link signature to device-link signature. +- Updated signature endpoints to use /device-link/ paths. +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackURL` field with regex validation. +- Added `deviceLinkBase` to session response. + ## [3.1.3] - 2025-06-13 ### Added diff --git a/README.md b/README.md index 9a37e564..029f1cb5 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ This library supports Smart-ID API v3.1. * [Dynamic link certificate choice session](#dynamic-link-certificate-choice-session) * [Examples of initiating a dynamic-link certificate choice session](#examples-of-initiating-a-dynamic-link-certificate-choice-session) * [Initiating dynamic-link certificate choice](#initiating-an-anonymous-certificate-choice-session) - * [Dynamic-link signature session](#dynamic-link-signature-session) - * [Examples of initiating a dynamic-link signature session](#examples-of-initiating-a-notification-based-signature-session) - * [Initiating a dynamic-link signature session using semantics identifier](#initiating-a-dynamic-link-signature-session-with-semantics-identifier) - * [Initiating a dynamic-link signature session using document number](#initiating-a-dynamic-link-signature-session-with-document-number) + * [Device-link signature session](#device-link-signature-session) + * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) + * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) + * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) * [Examples of allowed dynamic-link interactions order](#examples-of-allowed-dynamic-link-interactions-order) * [Additional request properties](#additional-dynamic-link-session-request-properties) * [Generating QR-code or device link](#generating-qr-code-or-device-link) @@ -362,11 +362,11 @@ Instant responseReceivedAt = certificateChoice.getReceivedAt(); Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Dynamic-link signature session +### Device-link signature session #### Request Parameters -The request parameters for the dynamic-link signature session are as follows: +The request parameters for the device-link signature session are as follows: * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. @@ -374,27 +374,31 @@ The request parameters for the dynamic-link signature session are as follows: * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. * `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of objects defining the allowed interactions in order of preference. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. * Each interaction object includes: * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `initialCallbackURL`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: requestProperties: +* `requestProperties`: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. #### Response Parameters -The response from a successful dynamic-link signature session creation contains the following parameters: +The response from a successful device-link signature session creation contains the following parameters: * `sessionID`: A string that can be used to request the session status result. * `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. -#### Examples of initiating a dynamic-link signature session +#### Examples of initiating a device-link signature session -##### Initiating a dynamic-link signature session with semantics identifier +##### Initiating a device-link signature session with semantics identifier ```java // Create the signable data @@ -408,13 +412,14 @@ var semanticsIdentifier = new SemanticsIdentifier( "40504040001" ); -// Initiate the dynamic-link signature -DynamicLinkSessionResponse signatureResponse = client.createDynamicLinkSignature() +// Initiate the device-link signature +DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); // Process the signature response @@ -423,14 +428,15 @@ String sessionToken = signatureResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = signatureResponse.getSessionSecret(); Instant receivedAt = signatureResponse.getReceivedAt(); +String deviceLinkBase = signatureResponse.getDeviceLinkBase(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret, receivedAt and deviceLinkBase provided in the signatureResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -##### Initiating a dynamic-link signature session with document number +##### Initiating a device-link signature session with document number ```java // Create the signable data @@ -440,13 +446,14 @@ signableData.setHashType(HashType.SHA256); // Specify the document number String documentNumber = "PNOEE-40504040001-MOCK-Q"; -// Build the dynamic-link signature request -DynamicLinkSessionResponse signatureResponse = client.createDynamicLinkSignature() +// Build the device-link signature request +DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); // Process the signature response @@ -456,11 +463,12 @@ String sessionToken = signatureResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = signatureResponse.getSessionSecret(); Instant receivedAt = signatureResponse.getReceivedAt(); +String deviceLinkBase = signatureResponse.getDeviceLinkBase(); -// Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureResponse +// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret, receivedAt and deviceLinkBase provided in the signatureResponse // Start querying sessions status ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Error Handling diff --git a/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java similarity index 63% rename from src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 4da6bfca..488146bc 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -34,7 +34,10 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.DeviceLinkUtil; import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; @@ -45,9 +48,10 @@ import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SignatureSessionRequest; -public class DynamicLinkSignatureSessionRequestBuilder { +public class DeviceLinkSignatureSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(DynamicLinkSignatureSessionRequestBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(DeviceLinkSignatureSessionRequestBuilder.class); + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; private final SmartIdConnector connector; @@ -58,19 +62,20 @@ public class DynamicLinkSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private List allowedInteractionsOrder; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; private SignableData signableData; private SignableHash signableHash; - private boolean certificateChoiceMade; + private String initialCallbackURL; /** * Constructs a new Smart-ID signature request builder with the given connector. * * @param connector the connector */ - public DynamicLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { + public DeviceLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { this.connector = connector; } @@ -80,7 +85,7 @@ public DynamicLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { * @param relyingPartyUUID the relying party UUID * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { this.relyingPartyUUID = relyingPartyUUID; return this; } @@ -91,7 +96,7 @@ public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String rel * @param relyingPartyName the relying party name * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { this.relyingPartyName = relyingPartyName; return this; } @@ -102,7 +107,7 @@ public DynamicLinkSignatureSessionRequestBuilder withRelyingPartyName(String rel * @param documentNumber the document number * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + public DeviceLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; return this; } @@ -113,7 +118,7 @@ public DynamicLinkSignatureSessionRequestBuilder withDocumentNumber(String docum * @param semanticsIdentifier the semantics identifier * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + public DeviceLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { this.semanticsIdentifier = semanticsIdentifier; return this; } @@ -124,7 +129,7 @@ public DynamicLinkSignatureSessionRequestBuilder withSemanticsIdentifier(Semanti * @param certificateLevel the certificate level * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + public DeviceLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; return this; } @@ -135,7 +140,7 @@ public DynamicLinkSignatureSessionRequestBuilder withCertificateLevel(Certificat * @param nonce the nonce * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withNonce(String nonce) { + public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { this.nonce = nonce; return this; } @@ -146,19 +151,31 @@ public DynamicLinkSignatureSessionRequestBuilder withNonce(String nonce) { * @param capabilities the capabilities * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { this.capabilities = Set.of(capabilities); return this; } /** - * Sets the allowed interactions order. + * Sets the hash algorithm to be used for signature creation. + * By default, SHA3-512 is used. * - * @param allowedInteractionsOrder the allowed interactions order + * @param hashAlgorithm the hash algorithm to use * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; + public DeviceLinkSignatureSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the interactions for device-link signature. + * + * @param interactions the interactions + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; return this; } @@ -168,7 +185,7 @@ public DynamicLinkSignatureSessionRequestBuilder withAllowedInteractionsOrder(Li * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + public DeviceLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; return this; } @@ -179,7 +196,7 @@ public DynamicLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(bool * @param signatureAlgorithm the signature algorithm * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; return this; } @@ -193,7 +210,7 @@ public DynamicLinkSignatureSessionRequestBuilder withSignatureAlgorithm(Signatur * @param signableData the data to be signed * @return this builder instance */ - public DynamicLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { this.signableData = signableData; return this; } @@ -207,27 +224,26 @@ public DynamicLinkSignatureSessionRequestBuilder withSignableData(SignableData s * @param signableHash the hash data to be signed * @return this builder */ - public DynamicLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { this.signableHash = signableHash; return this; } /** - * Marks whether a certificate choice has been made. + * Sets the initial callback URL. *

    - * This method allows specifying if a certificate selection was made prior to initiating this signing session. - * Once set to true, the signing request can proceed without further certificate selection. + * This URL is used to redirect the user after the signature session is completed. * - * @param certificateChoiceMade indicates if certificate choice has been made + * @param initialCallbackURL the initial callback URL * @return this builder instance */ - public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boolean certificateChoiceMade) { - this.certificateChoiceMade = certificateChoiceMade; + public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackURL(String initialCallbackURL) { + this.initialCallbackURL = initialCallbackURL; return this; } /** - * Sends the signature request and initiates a dynamic link-based signature session. + * Sends the signature request and initiates a device link-based signature session. *

    * There are two supported ways to start the signature session: *

      @@ -236,21 +252,23 @@ public DynamicLinkSignatureSessionRequestBuilder withCertificateChoiceMade(boole *
    * * @return a {@link DeviceLinkSessionResponse} containing session details such as - * session ID, session token, and session secret. + * session ID, session token, session secret and device link base URL. + * @throws SmartIdClientException if request parameters are invalid + * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initSignatureSession() { validateParameters(); SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - DeviceLinkSessionResponse dynamicLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); - validateResponseParameters(dynamicLinkSignatureSessionResponse); - return dynamicLinkSignatureSessionResponse; + DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + validateResponseParameters(deviceLinkSignatureSessionResponse); + return deviceLinkSignatureSessionResponse; } private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { if (documentNumber != null) { - return connector.initDynamicLinkSignature(request, documentNumber); + return connector.initDeviceLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { - return connector.initDynamicLinkSignature(request, semanticsIdentifier); + return connector.initDeviceLinkSignature(request, semanticsIdentifier); } else { throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set. Anonymous signing is not allowed."); } @@ -270,9 +288,15 @@ private SignatureSessionRequest createSignatureSessionRequest() { signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); } signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); + + var signatureAlgorithmParameters = new SignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm.getValue()); + signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + request.setSignatureProtocolParameters(signatureProtocolParameters); + request.setNonce(nonce); - request.setAllowedInteractionsOrder(allowedInteractionsOrder); + request.setInteractions(DeviceLinkUtil.encodeToBase64(interactions)); if (this.shareMdClientIpAddress != null) { var requestProperties = new RequestProperties(); @@ -280,6 +304,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { request.setRequestProperties(requestProperties); } request.setCapabilities(capabilities); + request.setInitialCallbackURL(initialCallbackURL); return request; } @@ -290,21 +315,27 @@ private void validateParameters() { if (relyingPartyName == null || relyingPartyName.isEmpty()) { throw new SmartIdClientException("Relying Party Name must be set."); } - validateAllowedInteractions(); + validateInteractions(); + validateInitialCallbackURL(); - if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); } - if (certificateChoiceMade) { - throw new SmartIdClientException("Certificate choice was made before using this method. Cannot proceed with signature request."); + } + + private void validateInteractions() { + if (interactions == null || interactions.isEmpty()) { + logger.error("Parameter interactions must be set and contain at least one interaction."); + throw new SmartIdClientException("Parameter interactions must be set and contain at least one interaction."); } + validateNoDuplicateInteractions(); + interactions.forEach(DeviceLinkInteraction::validate); } - private void validateAllowedInteractions() { - if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { - throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); + private void validateInitialCallbackURL() { + if (!StringUtil.isEmpty(initialCallbackURL) && !initialCallbackURL.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdClientException("initialCallbackURL must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } - allowedInteractionsOrder.forEach(Interaction::validate); } private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { @@ -322,9 +353,16 @@ private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSign logger.error("Session secret is missing from the response"); throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); } - if (deviceLinkSignatureSessionResponse.getDeviceLinkBase() == null) { - throw new SmartIdClientException("deviceLinkBase is missing from the response"); + if (deviceLinkSignatureSessionResponse.getDeviceLinkBase() == null || deviceLinkSignatureSessionResponse.getDeviceLinkBase().toString().isBlank()) { + logger.error("deviceLinkBase is missing or empty in the response"); + throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing from the response"); } + } + private void validateNoDuplicateInteractions() { + if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + logger.error("Duplicate values found in interactions"); + throw new SmartIdClientException("Duplicate values in interactions are not allowed"); + } } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 9a190334..904ed8c5 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -35,6 +35,8 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.NotificationUtil; import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; @@ -258,7 +260,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); request.setSignatureProtocolParameters(signatureProtocolParameters); request.setNonce(nonce); - request.setAllowedInteractionsOrder(allowedInteractionsOrder); + request.setInteractions(NotificationUtil.encodeToBase64(allowedInteractionsOrder)); if (this.shareMdClientIpAddress != null) { var requestProperties = new RequestProperties(); diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index a0f12198..240ef94e 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -112,12 +112,12 @@ public NotificationAuthenticationSessionRequestBuilder createNotificationAuthent } /** - * Creates a new builder for creating a new dynamic link signature session request + * Creates a new builder for creating a new device link signature session request * - * @return builder for creating a new dynamic link signature session request + * @return builder for creating a new device link signature session request */ - public DynamicLinkSignatureSessionRequestBuilder createDynamicLinkSignature() { - return new DynamicLinkSignatureSessionRequestBuilder(getSmartIdConnector()) + public DeviceLinkSignatureSessionRequestBuilder createDeviceLinkSignature() { + return new DeviceLinkSignatureSessionRequestBuilder(getSmartIdConnector()) .withRelyingPartyUUID(relyingPartyUUID) .withRelyingPartyName(relyingPartyName); } diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index aea29979..cca90805 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -90,22 +90,22 @@ public interface SmartIdConnector extends Serializable { CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); /** - * Initiates a dynamic link based signature sessions. + * Initiates a device link based signature sessions. * * @param request SignatureSessionRequest containing necessary parameters for the signature session * @param semanticsIdentifier The semantics identifier - * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** - * Initiates a dynamic link based signature sessions. + * Initiates a device link based signature sessions. * * @param request SignatureSessionRequest containing necessary parameters for the signature session * @param documentNumber The document number - * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber); + DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, String documentNumber); /** * Initiates a notification-based signature session using a semantics identifier. diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 464a055b..cfa15217 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -83,8 +83,8 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; - private static final String DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/dynamic-link/etsi"; - private static final String DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/dynamic-link/document"; + private static final String DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/device-link/etsi"; + private static final String DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/device-link/document"; private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; @@ -96,7 +96,6 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; - private final String endpointUrl; private transient Configuration clientConfig; private transient Client configuredClient; @@ -211,23 +210,23 @@ public CertificateResponse getCertificateByDocumentNumber(String documentNumber, } @Override - public DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) - .path(DYNAMIC_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postDynamicLinkSignatureRequest(uri, request); + return postDeviceLinkSignatureRequest(uri, request); } @Override - public DeviceLinkSessionResponse initDynamicLinkSignature(SignatureSessionRequest request, String documentNumber) { + public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) - .path(DYNAMIC_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postDynamicLinkSignatureRequest(uri, request); + return postDeviceLinkSignatureRequest(uri, request); } public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { @@ -359,7 +358,7 @@ private CertificateResponse postCertificateByDocumentNumberRequest(URI uri, Cert } } - private DeviceLinkSessionResponse postDynamicLinkSignatureRequest(URI uri, SignatureSessionRequest request) { + private DeviceLinkSessionResponse postDeviceLinkSignatureRequest(URI uri, SignatureSessionRequest request) { try { return postRequest(uri, request, DeviceLinkSessionResponse.class); } catch (NotFoundException ex) { diff --git a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index ab900a21..07c8281a 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -32,6 +32,7 @@ public class RawDigestSignatureProtocolParameters implements Serializable { private String digest; private String signatureAlgorithm; + private SignatureAlgorithmParameters signatureAlgorithmParameters; public String getDigest() { return digest; @@ -48,4 +49,12 @@ public String getSignatureAlgorithm() { public void setSignatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } + + public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java index 6ba4f353..5eb10da1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java @@ -27,7 +27,6 @@ */ import java.io.Serializable; -import java.util.List; import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; @@ -41,7 +40,7 @@ public class SignatureSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String certificateLevel; - private final SignatureProtocol signatureProtocol = SignatureProtocol.RAW_DIGEST_SIGNATURE; + private final String signatureProtocol = SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); private RawDigestSignatureProtocolParameters signatureProtocolParameters; @@ -52,11 +51,14 @@ public class SignatureSessionRequest implements Serializable { private Set capabilities; @JsonInclude(JsonInclude.Include.NON_EMPTY) - private List allowedInteractionsOrder; + private String interactions; @JsonInclude(JsonInclude.Include.NON_NULL) private RequestProperties requestProperties; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String initialCallbackURL; + public String getRelyingPartyUUID() { return relyingPartyUUID; } @@ -81,7 +83,7 @@ public void setCertificateLevel(String certificateLevel) { this.certificateLevel = certificateLevel; } - public SignatureProtocol getSignatureProtocol() { + public String getSignatureProtocol() { return signatureProtocol; } @@ -109,12 +111,12 @@ public void setCapabilities(Set capabilities) { this.capabilities = capabilities; } - public List getAllowedInteractionsOrder() { - return allowedInteractionsOrder; + public String getInteractions() { + return interactions; } - public void setAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; + public void setInteractions(String interactions) { + this.interactions = interactions; } public RequestProperties getRequestProperties() { @@ -124,4 +126,12 @@ public RequestProperties getRequestProperties() { public void setRequestProperties(RequestProperties requestProperties) { this.requestProperties = requestProperties; } + + public String getInitialCallbackURL() { + return initialCallbackURL; + } + + public void setInitialCallbackURL(String initialCallbackURL) { + this.initialCallbackURL = initialCallbackURL; + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/util/NotificationUtil.java similarity index 62% rename from src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java rename to src/main/java/ee/sk/smartid/util/NotificationUtil.java index e47a299f..e632801f 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/util/NotificationUtil.java @@ -1,4 +1,4 @@ -package ee.sk.smartid; +package ee.sk.smartid.util; /*- * #%L @@ -26,25 +26,25 @@ * #L% */ -import java.io.Serializable; -import java.util.Set; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.NotificationInteraction; -public class SignatureAlgorithmParameters implements Serializable { +public class NotificationUtil { - private static final Set SUPPORTED_HASH_ALGORITHMS = Set.of("SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"); + private static final ObjectMapper mapper = new ObjectMapper(); - private String hashAlgorithm; - - public String getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(String hashAlgorithm) { - if (!SUPPORTED_HASH_ALGORITHMS.contains(hashAlgorithm)) { - throw new SmartIdClientException("Unsupported hashAlgorithm: " + hashAlgorithm + ". Supported values: " + SUPPORTED_HASH_ALGORITHMS); + public static String encodeToBase64(List interactions) { + try { + String json = mapper.writeValueAsString(interactions); + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException e) { + throw new SmartIdClientException("Unable to encode interactions to base64", e); } - this.hashAlgorithm = hashAlgorithm; } } diff --git a/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java similarity index 64% rename from src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java rename to src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index a11b2f25..7053789e 100644 --- a/src/test/java/ee/sk/smartid/DynamicLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -37,13 +37,13 @@ import static org.mockito.Mockito.when; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -57,28 +57,30 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.SignatureSessionRequest; -@Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-105") -class DynamicLinkSignatureSessionRequestBuilderTest { +class DeviceLinkSignatureSessionRequestBuilderTest { private SmartIdConnector connector; - private DynamicLinkSignatureSessionRequestBuilder builder; + private DeviceLinkSignatureSessionRequestBuilder builder; @BeforeEach void setUp() { connector = mock(SmartIdConnector.class); - builder = new DynamicLinkSignatureSessionRequestBuilder(connector) + builder = new DeviceLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())) - .withCertificateChoiceMade(false); + .withInitialCallbackURL("https://example.com/callback"); } @Test @@ -86,7 +88,7 @@ void initSignatureSession_withSemanticsIdentifier() { var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); builder.withSemanticsIdentifier(semanticsIdentifier); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -97,9 +99,9 @@ void initSignatureSession_withSemanticsIdentifier() { assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); } @Test @@ -107,7 +109,7 @@ void initSignatureSession_withDocumentNumber() { String documentNumber = "PNOEE-31111111111"; builder.withDocumentNumber(documentNumber); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -118,9 +120,9 @@ void initSignatureSession_withDocumentNumber() { assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), eq(documentNumber)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(documentNumber)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); } @ParameterizedTest @@ -128,18 +130,18 @@ void initSignatureSession_withDocumentNumber() { void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.getCertificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); } @ParameterizedTest @@ -147,37 +149,37 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel void initSignatureSession_withNonce_ok(String nonce) { builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.getNonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); } @Test void initSignatureSession_withRequestProperties() { builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertNotNull(capturedRequest.getRequestProperties()); assertTrue(capturedRequest.getRequestProperties().getShareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @Test @@ -186,19 +188,19 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { signableData.setHashType(HashType.SHA256); builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @ParameterizedTest @@ -209,18 +211,18 @@ void initSignatureSession_withSignableHash(HashType hashType) { signableHash.setHashType(hashType); builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @ParameterizedTest @@ -228,39 +230,18 @@ void initSignatureSession_withSignableHash(HashType hashType) { void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); - } - - @ParameterizedTest - @EnumSource(HashType.class) - void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(hashType); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - - DeviceLinkSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @Test @@ -269,14 +250,14 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { signableData.setHashType(HashType.SHA512); builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDynamicLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); @@ -303,19 +284,11 @@ void initSignatureSession_whenHashTypeIsNull_throwsException() { assertEquals("HashType must be set for signableData.", ex.getMessage()); } - @Test - void initSignatureSession_whenCertificateChoiceMade() { - builder.withCertificateChoiceMade(true); - - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Certificate choice was made before using this method. Cannot proceed with signature request.", ex.getMessage()); - } - @Test void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlgorithm() { builder.withSignableHash(null).withSignableData(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); @@ -323,11 +296,46 @@ void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlg @ParameterizedTest @NullAndEmptySource - void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { - builder.withAllowedInteractionsOrder(allowedInteractionsOrder); + void initSignatureSession_whenInteractionsIsNullOrEmpty(List interactions) { + builder.withInteractions(interactions); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Allowed interactions order must be set and contain at least one interaction.", ex.getMessage()); + assertEquals("Parameter interactions must be set and contain at least one interaction.", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSignableData(new SignableData("test".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in"))) + .withInitialCallbackURL(url) + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) + .initSignatureSession() + ); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateInteractionsProvider.class) + void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withSignableData(new SignableData("data".getBytes(StandardCharsets.UTF_8))) + .withInteractions(duplicateInteractions) + .initSignatureSession() + ); + + assertEquals("Duplicate values in interactions are not allowed", exception.getMessage()); } @ParameterizedTest @@ -382,9 +390,10 @@ void validateResponseParameters_missingSessionID(String sessionID) { response.setSessionID(sessionID); response.setSessionToken("test-session-token"); response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session ID is missing from the response", ex.getMessage()); @@ -397,9 +406,10 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { response.setSessionID("test-session-id"); response.setSessionToken(sessionToken); response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session token is missing from the response", ex.getMessage()); @@ -412,13 +422,30 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { response.setSessionID("test-session-id"); response.setSessionToken("test-session-token"); response.setSessionSecret(sessionSecret); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDynamicLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); assertEquals("Session secret is missing from the response", ex.getMessage()); } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + + builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + assertEquals("deviceLinkBase is missing from the response", ex.getMessage()); + } } private DeviceLinkSessionResponse mockSignatureSessionResponse() { @@ -458,4 +485,27 @@ public Stream provideArguments(ExtensionContext context) { return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); } } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + ); + } + } + + static class DuplicateInteractionsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + var interaction1 = DeviceLinkInteraction.displayTextAndPIN("Sign this."); + var interaction2 = DeviceLinkInteraction.displayTextAndPIN("Sign this again."); + return Stream.of( + Arguments.of(List.of(interaction1, interaction1)), + Arguments.of(List.of(interaction1, interaction2)) + ); + } + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 5362f04b..5e7ace37 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -123,7 +123,6 @@ void createDeviceLinkAuthentication_anonymous() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -141,7 +140,6 @@ void createDeviceLinkAuthentication_withDocumentNumber() { .withDocumentNumber("PNOEE-1234567890-MOCK-Q") .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -159,7 +157,6 @@ void createDeviceLinkAuthentication_withSemanticsIdentifier() { .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); @@ -170,50 +167,53 @@ void createDeviceLinkAuthentication_withSemanticsIdentifier() { } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-105") @Nested @WireMockTest(httpPort = 18089) - class DynamicLinkSignatureSession { + class DeviceLinkSignatureSession { @Test - void createDynamicLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); + void createDeviceLinkSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-signature-request.json", "responses/device-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) + .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getDeviceLinkBase()); assertNotNull(response.getReceivedAt()); } @Test - void createDynamicLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/dynamic-link/etsi/PNOEE-1234567890", "requests/device-link-signature-request.json", "responses/dynamic-link-signature-session-response.json"); + void createDeviceLinkSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", "requests/device-link-signature-request.json", "responses/device-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); - DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) + .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getDeviceLinkBase()); assertNotNull(response.getReceivedAt()); } } diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 7f9f4d12..5e912610 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -339,13 +339,13 @@ void signature_withSemanticIdentifier() { "40504040001"); // identifier (according to country and identity type reference) // Build the dynamic link signature request - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDynamicLinkSignature() + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) .withRelyingPartyName(smartIdClient.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withAllowedInteractionsOrder(List.of( + .withInteractions(List.of( DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .initSignatureSession(); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 49fb0127..a6345af0 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -161,11 +161,11 @@ void initDynamicLinkCertificateChoice() { class Signature { @Test - void initDynamicLinkSignature_withSemanticIdentifier() { + void initDeviceLinkSignature_withSemanticIdentifier() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); @@ -173,7 +173,7 @@ void initDynamicLinkSignature_withSemanticIdentifier() { signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -182,11 +182,11 @@ void initDynamicLinkSignature_withSemanticIdentifier() { } @Test - void initDynamicLinkSignature_withDocumentNumber() { + void initDeviceLinkSignature_withDocumentNumber() { var request = new SignatureSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); @@ -194,7 +194,7 @@ void initDynamicLinkSignature_withDocumentNumber() { signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); @@ -304,7 +304,7 @@ private static SignatureSessionRequest toSignatureSessionRequest() { signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); signatureProtocolParameters.setDigest(digest); request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Sign it!"))); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); return request; } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 0d3eecdd..9c400def 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -41,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.net.URI; import java.time.Instant; import java.util.List; import java.util.concurrent.TimeUnit; @@ -659,9 +660,9 @@ void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { @Nested @WireMockTest(httpPort = 18089) - class DynamicLinkSignatureTests { + class DeviceLinkSignatureTests { - private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/dynamic-link/etsi/PNOEE-31111111111"; + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; private SmartIdRestConnector connector; @@ -672,57 +673,59 @@ public void setUp() { } @Test - void initDynamicLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/dynamic-link-signature-session-response.json"); + void initDeviceLinkSignature_withSemanticsIdentifier_successful() { + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/device-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - DeviceLinkSessionResponse response = connector.initDynamicLinkSignature(request, semanticsIdentifier); + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, semanticsIdentifier); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); assertEquals("test-session-token", response.getSessionToken()); assertEquals("test-session-secret", response.getSessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.getDeviceLinkBase()); } @Test - void initDynamicLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/dynamic-link/document/PNOEE-31111111111-MOCK-Q", "responses/dynamic-link-signature-session-response.json"); + void initDeviceLinkSignature_withDocumentNumber_successful() { + stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/device-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111-MOCK-Q"; - DeviceLinkSessionResponse response = connector.initDynamicLinkSignature(request, documentNumber); + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); assertNotNull(response); assertEquals("test-session-id", response.getSessionID()); assertEquals("test-session-token", response.getSessionToken()); assertEquals("test-session-secret", response.getSessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.getDeviceLinkBase()); } @Test - void initDynamicLinkSignature_userAccountNotFound() { + void initDeviceLinkSignature_userAccountNotFound() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(UserAccountNotFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkSignature_relyingPartyNoPermission() { + void initDeviceLinkSignature_relyingPartyNoPermission() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkSignature_invalidRequest() { + void initDeviceLinkSignature_invalidRequest() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); SignatureSessionRequest request = new SignatureSessionRequest(); @@ -730,61 +733,61 @@ void initDynamicLinkSignature_invalidRequest() { request.setRelyingPartyName(""); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); - assertEquals("Request is unauthorized for URI http://localhost:18089/signature/dynamic-link/etsi/PNOEE-31111111111", exception.getMessage()); + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); } @Test - void initDynamicLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkSignature_throwsPersonShouldViewSmartIdPortalException() { + void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } @Test - void initDynamicLinkSignature_throwsSmartIdClientException() { + void initDeviceLinkSignature_throwsSmartIdClientException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @Test - void initDynamicLinkSignature_throwsServerMaintenanceException() { + void initDeviceLinkSignature_throwsServerMaintenanceException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(ServerMaintenanceException.class, () -> connector.initDynamicLinkSignature(request, semanticsIdentifier)); + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); } } @@ -1060,11 +1063,13 @@ private static SignatureSessionRequest createSignatureSessionRequest() { var protocolParameters = new RawDigestSignatureProtocolParameters(); protocolParameters.setDigest("base64-encoded-digest"); - protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); + protocolParameters.setSignatureAlgorithm("rsassa-pss"); + protocolParameters.setSignatureAlgorithm("SHA3-512"); request.setSignatureProtocolParameters(protocolParameters); - request.setAllowedInteractionsOrder(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign the document"))); + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Sign the document"); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); return request; } @@ -1076,10 +1081,13 @@ private static SignatureSessionRequest createNotificationSignatureSessionRequest var protocolParameters = new RawDigestSignatureProtocolParameters(); protocolParameters.setDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="); + protocolParameters.setSignatureAlgorithm("rsassa-pss"); protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); request.setSignatureProtocolParameters(protocolParameters); - request.setAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))); + + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Verify the code"); + request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); return request; } diff --git a/src/test/resources/requests/device-link-signature-request.json b/src/test/resources/requests/device-link-signature-request.json index 75fae51f..2a87971f 100644 --- a/src/test/resources/requests/device-link-signature-request.json +++ b/src/test/resources/requests/device-link-signature-request.json @@ -4,12 +4,10 @@ "signatureProtocol": "RAW_DIGEST_SIGNATURE", "signatureProtocolParameters": { "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "sha512WithRSAEncryption" - }, - "interactions": [ - { - "type": "displayTextAndPIN", - "displayText60": "Sign document?" + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" } - ] + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d" } \ No newline at end of file diff --git a/src/test/resources/responses/dynamic-link-signature-session-response.json b/src/test/resources/responses/device-link-signature-session-response.json similarity index 80% rename from src/test/resources/responses/dynamic-link-signature-session-response.json rename to src/test/resources/responses/device-link-signature-session-response.json index 6bb12632..7caaa454 100644 --- a/src/test/resources/responses/dynamic-link-signature-session-response.json +++ b/src/test/resources/responses/device-link-signature-session-response.json @@ -2,5 +2,6 @@ "sessionID": "test-session-id", "sessionToken": "test-session-token", "sessionSecret": "test-session-secret", + "deviceLinkBase": "https://smart-id.com/device-link/", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file From 1ce7e08ba20ad301332920d042e3a0a93c122b85 Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Mon, 7 Jul 2025 06:56:26 +0300 Subject: [PATCH 26/57] SLIB-105 - Replaced dynamic link signature flows with device link signature (#116) * SLIB-93 - change auth session initialization from dynamic-link to device-link * SLIB-93 - refactored, added tests, improved javadoc-s * SLIB-93 - refactored, fixed tests, updated README * SLIB-93 - interactions refacto, fixed javadocs, README * SLIB-93 - renamed withSignatureAlgorithmParameters method to withHashAlgorithm * SLIB-98 - added new certificate choice endpoint * SLIB-98 - reverted AuthenticationResponseValidator changes * SLIB-98 - refactored, fixed javadocs, README * SLIB-98 - README fixes * SLIB-98 - added certificateLevel validation, refactored README * SLIB-98 - refactored state error handling * SLIB-98 - refactored certificate querying logic * SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding * Revert "SLIB-99 - fixed scheme_name, elapsedSeconds placement, RPname/BrokedRPname encoding" This reverts commit f4356ba8603872fd35013cb569e604357c932a8e. * SLIB-98 - refactored, code review fixes * SLIB-98 - corrected unknown json fail * SLIB-98 - fixed naming * SLIB-105 - replaced dynamic-link signature flows with device-link signature * SLIB-105 - added missing initialCallbackURL to request * SLIB-105 - removed withCertificateChoice, fixed tests, README * SLIB-105 - changed hasAlgorithm default to SHA-512 * SLIB-105 - fixed hasAlgorithm in test json fail --------- Co-authored-by: ragnar.haide --- README.md | 4 ++-- .../smartid/DeviceLinkSignatureSessionRequestBuilder.java | 2 +- .../DeviceLinkSignatureSessionRequestBuilderTest.java | 6 +++--- src/test/java/ee/sk/smartid/SmartIdClientTest.java | 4 ++-- .../resources/requests/device-link-signature-request.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 029f1cb5..2eb6c48a 100644 --- a/README.md +++ b/README.md @@ -417,7 +417,7 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); @@ -451,7 +451,7 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) - .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 488146bc..3fe8d12a 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -62,7 +62,7 @@ public class DeviceLinkSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA_512; private List interactions; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 7053789e..123b2bca 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -77,7 +77,7 @@ void setUp() { .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())) .withInitialCallbackURL("https://example.com/callback"); @@ -312,7 +312,7 @@ void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url, .withRelyingPartyName("DEMO") .withSignableData(new SignableData("test".getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in"))) .withInitialCallbackURL(url) .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) @@ -329,7 +329,7 @@ void initSignatureSession_duplicateInteractions_shouldThrowException(List Date: Mon, 7 Jul 2025 12:54:36 +0300 Subject: [PATCH 27/57] Update authentication response validator (#117) * SLIB-95 - update USER_REFUSED_INTERACTION handling * SLIB-95 - update session status * SLIB-95 - add new end result error responses with handling * SLIB-95 - update validation message for session status fields; add new session status field validations * SLIB-95 - convert authentication request objects to records * SLIB-95 - update authentication signature value validations * SLIB-95 - update certificate value and signature value validations for device-link authentication * SLIB-95 - update tests and validations errors * SLIB-95 - improve trust anchor and intermediate certificate validations * SLIB-95 - improve code quality; update README * SLIB-95 - fix logging message * SLIB-95 - add license headers --------- Co-authored-by: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> --- CHANGELOG.md | 18 + MIGRATION_GUIDE.md | 9 +- README.md | 238 ++--- .../ee/sk/smartid/AuthenticationResponse.java | 101 +- .../smartid/AuthenticationResponseMapper.java | 136 +-- .../AuthenticationResponseValidator.java | 333 +++---- .../CertificateChoiceResponseMapper.java | 17 +- .../DefaultAuthenticationResponseMapper.java | 286 ++++++ .../sk/smartid/DefaultTrustedCACertStore.java | 86 ++ .../smartid/DefaultTrustedCAStoreBuilder.java | 158 ++++ ...nkAuthenticationSessionRequestBuilder.java | 59 +- ...iceLinkSignatureSessionRequestBuilder.java | 22 +- ...ertificateChoiceSessionRequestBuilder.java | 5 +- .../ee/sk/smartid/ErrorResultHandler.java | 35 +- .../sk/smartid/FileTrustedCAStoreBuilder.java | 219 +++++ src/main/java/ee/sk/smartid/FlowType.java | 51 + .../java/ee/sk/smartid/HashAlgorithm.java | 62 ++ .../java/ee/sk/smartid/MaskGenAlgorithm.java | 60 ++ ...onAuthenticationSessionRequestBuilder.java | 49 +- ...ertificateChoiceSessionRequestBuilder.java | 3 +- ...icationSignatureSessionRequestBuilder.java | 12 +- .../ee/sk/smartid/SignatureAlgorithm.java | 14 +- .../sk/smartid/SignatureResponseMapper.java | 16 +- src/main/java/ee/sk/smartid/TrailerField.java | 60 ++ .../ee/sk/smartid/TrustedCACertStore.java | 56 ++ .../ExpectedLinkedSessionException.java | 36 + .../permanent/ProtocolFailureException.java | 36 + .../permanent/SmartIdServerException.java | 36 + ...InteractionNotSupportedByAppException.java | 6 +- .../AcspV2SignatureProtocolParameters.java | 36 +- .../dao/AuthenticationSessionRequest.java | 100 +- .../smartid/rest/dao/RequestProperties.java | 24 +- .../rest/dao/SessionMaskGenAlgorithm.java | 54 ++ .../SessionMaskGenAlgorithmParameters.java | 46 + .../ee/sk/smartid/rest/dao/SessionResult.java | 41 +- .../rest/dao/SessionResultDetails.java | 45 + .../sk/smartid/rest/dao/SessionSignature.java | 27 + .../SessionSignatureAlgorithmParameters.java | 72 ++ .../ee/sk/smartid/rest/dao/SessionStatus.java | 15 +- .../dao/SignatureAlgorithmParameters.java | 12 +- .../AuthenticationResponseMapperTest.java | 343 ------- .../AuthenticationResponseValidatorTest.java | 340 +++---- .../CertificateChoiceResponseMapperTest.java | 20 +- ...faultAuthenticationResponseMapperTest.java | 892 ++++++++++++++++++ .../DefaultTrustedCAStoreBuilderTest.java | 81 ++ ...thenticationSessionRequestBuilderTest.java | 40 +- ...inkSignatureSessionRequestBuilderTest.java | 6 +- .../ee/sk/smartid/ErrorResultHandlerTest.java | 76 +- .../FileDefaultTrustedCAStoreBuilderTest.java | 102 ++ ...thenticationSessionRequestBuilderTest.java | 22 +- ...ficateChoiceSessionRequestBuilderTest.java | 2 +- ...ionSignatureSessionRequestBuilderTest.java | 21 +- ...essionEndResultErrorArgumentsProvider.java | 22 +- .../smartid/SignatureResponseMapperTest.java | 30 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 4 +- ...erRefusedInteractionArgumentsProvider.java | 50 + .../integration/ReadmeIntegrationTest.java | 238 +++-- .../SmartIdRestIntegrationTest.java | 68 +- .../rest/SmartIdRestConnectorTest.java | 215 +++-- ...ession-status-expected-linked-session.json | 8 + .../session-status-protocol-failure.json | 8 + .../session-status-server-error.json | 8 + ...sion-status-successful-authentication.json | 28 +- .../session-status-successful-signature.json | 2 +- ...s-user-refused-confirmation-vc-choice.json | 10 +- ...sion-status-user-refused-confirmation.json | 10 +- ...tus-user-refused-display-text-and-pin.json | 10 +- ...session-status-user-refused-vc-choice.json | 10 +- .../sid_trust_anchor_certificates.jks | Bin 0 -> 4541 bytes .../test-certs/TEST_SK_ROOT_G1_2021E.pem.crt | 17 + .../TEST_of_SK_OCSP_RESPONDER_2020.pem.cer | 28 + src/test/resources/trusted_certificates.jks | Bin 0 -> 4573 bytes 72 files changed, 3864 insertions(+), 1538 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java create mode 100644 src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java create mode 100644 src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java create mode 100644 src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java create mode 100644 src/main/java/ee/sk/smartid/FlowType.java create mode 100644 src/main/java/ee/sk/smartid/HashAlgorithm.java create mode 100644 src/main/java/ee/sk/smartid/MaskGenAlgorithm.java create mode 100644 src/main/java/ee/sk/smartid/TrailerField.java create mode 100644 src/main/java/ee/sk/smartid/TrustedCACertStore.java create mode 100644 src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java create mode 100644 src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java create mode 100644 src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java delete mode 100644 src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java create mode 100644 src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java create mode 100644 src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java create mode 100644 src/test/resources/responses/session-status-expected-linked-session.json create mode 100644 src/test/resources/responses/session-status-protocol-failure.json create mode 100644 src/test/resources/responses/session-status-server-error.json create mode 100644 src/test/resources/sid_trust_anchor_certificates.jks create mode 100644 src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt create mode 100644 src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer create mode 100644 src/test/resources/trusted_certificates.jks diff --git a/CHANGELOG.md b/CHANGELOG.md index a3012c1b..9c7ab18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.3] - 2025-07-05 + +### Changed +- Updates to session status response + - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. + - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling + - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` + - Renamed `interactionFlowUsed` to `interactionTypeUsed`. +- Updated AuthenticationSessionRequest and related classes to records. +- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. + - Created to builder-classes for loading trusted CA certificates + - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore + - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates +- Refactored AuthenticationResponseMapper to be used as singleton instead of static class and added it as dependency for AuthenticationResponseValidator +- Update AuthenticationResponseValidator + - update signature value validation + - added additional certificate validations (validate certificate chain and certificate purpose) + ## [3.1.5] - 2025-06-30 - Renamed dynamic-link signature to device-link signature. diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6d2da39c..fbe17296 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -6,12 +6,9 @@ Some classes could also be used in v3 and for those classes the package did not # Migrating from Smart-ID v2 to Smart-ID v3 API -For Smart-ID v3 API replace `ee.sk.smartid.v2.SmartIdClient` with `ee.sk.smartid.SmartIdClient`. - ## Migrating authentication -To migrate from Smart-ID v2 to Smart-ID v3 authentication you need to change the following: -`ee.sk.smartid.SmartIdClient` provides methods `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create session request builders. +Smart-ID v3 authentication offers new methods to construct builders `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create authentication session request builders. It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. ### Overview of V2 authentication flow @@ -31,8 +28,8 @@ It is recommended to start using device-link authentication flows from Smart-ID 3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. 4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. -5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseMapper`, that will validate required fields and will also handler errors. `AuthenticationResponse` will be returned when everything is ok. -6. Finally use `ee.sk.smartid.AuthenticationResponseValidator` to validate the certificate and the signature in the response. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. +5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. +6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. ## Migrating signing diff --git a/README.md b/README.md index 2eb6c48a..2ef14ad4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This library supports Smart-ID API v3.1. * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) - * [Initiating a dynamic-link authentication session with document number](#initiating-a-dynamic-link-authentication-session-with-document-number) + * [Initiating a dynamic-link authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) * [Dynamic link certificate choice session](#dynamic-link-certificate-choice-session) * [Examples of initiating a dynamic-link certificate choice session](#examples-of-initiating-a-dynamic-link-certificate-choice-session) * [Initiating dynamic-link certificate choice](#initiating-an-anonymous-certificate-choice-session) @@ -34,8 +34,8 @@ This library supports Smart-ID API v3.1. * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) - * [Examples of allowed dynamic-link interactions order](#examples-of-allowed-dynamic-link-interactions-order) - * [Additional request properties](#additional-dynamic-link-session-request-properties) + * [Examples of allowed device-link interaction](#examples-of-device-link-interactions) + * [Additional request properties](#additional-device-link-session-request-properties) * [Generating QR-code or device link](#generating-qr-code-or-device-link) * [Generating device link ](#generating-device-link) * [Device link parameters](#device-link-parameters) @@ -139,27 +139,15 @@ logging.level.ee.sk.smartid.rest.LoggingFilter: trace ## Setting up SmartIdClient for v3.1 ```java -import ee.sk.smartid.SmartIdClient; - InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore. - -load(is, "changeit".toCharArray()); - -var client = new SmartIdClient(); -client. - -setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); -client. +trustStore.load(is, "changeit".toCharArray()); -setRelyingPartyName("DEMO"); -client. - -setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -client. - -setTrustStore(trustStore); +var smartIdClient = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setTrustStore(trustStore); ``` ## Device-link flows @@ -209,30 +197,35 @@ String rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DeviceLinkSessionResponse authenticationSessionResponse = client +// Setup builder +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRpChallenge(rpChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withSignatureProtocol(SignatureProtocol.ACSP_V2) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .initAuthenticationSession(); + )); + +// Init authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); +// Use sessionID to start polling for session status String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +// Following values are used for generating device link or QR-code String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); -String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse -// Start querying sessions status +// Next steps: +// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -254,30 +247,34 @@ String rpChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -DeviceLinkSessionResponse authenticationSessionResponse = client +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withSignatureProtocol(SignatureProtocol.ACSP_V2) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .initAuthenticationSession(); + )); + +// Init authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); +// Use sessionID to start polling for session status String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +// Following values are used for generating device link or QR-code String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); -String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse -// Start querying sessions status +// Next steps: +// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -292,30 +289,34 @@ String rpChallenge = RpChallengeGenerator.generate(); // Store generated randomChallenge only on backend side. Do not expose it to the client side. // Used for validating OK authentication sessions status response -DeviceLinkSessionResponse authenticationSessionResponse = client +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) .withRpChallenge(rpChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withSignatureProtocol(SignatureProtocol.ACSP_V2) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .initAuthenticationSession(); + )); + +// Init authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); +// Use sessionID to start polling for session status String sessionId = authenticationSessionResponse.getSessionID(); -// SessionID is used to query sessions status later +// Following values are used for generating device link or QR-code String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); -String deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); -// Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse -// Start querying sessions status +// Next steps: +// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -419,7 +420,7 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withSemanticsIdentifier(semanticsIdentifier) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .withInitialCallbackURL("https://example.com/callback") + .withInitialCallbackURL("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) .initSignatureSession(); // Process the signature response @@ -447,13 +448,12 @@ signableData.setHashType(HashType.SHA256); String documentNumber = "PNOEE-40504040001-MOCK-Q"; // Build the device-link signature request -DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() +DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .withInitialCallbackURL("https://example.com/callback") .initSignatureSession(); // Process the signature response @@ -490,7 +490,7 @@ try { } ``` -### Additional dynamic-link session request properties +### Additional device-link session request properties #### Using request properties to request the IP address of the user's device @@ -504,7 +504,7 @@ DynamicLinkSessionResponse authenticationSessionResponse = client .createDeviceLinkAuthentication() .withRandomChallenge(randomChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withAllowedInteractionsOrder(Collections.singletonList( + .withInteractions(Collections.singletonList( DynamicLinkInteraction.displayTextAndPIN("Log in?") )) // setting property to request the IP-address of the user's device @@ -512,7 +512,8 @@ DynamicLinkSessionResponse authenticationSessionResponse = client .initAuthenticationSession(); ``` -### Examples of allowed dynamic-link interactions order +### Examples of device link interactions +Todo: needs to be updated An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. For dynamic link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. @@ -524,25 +525,25 @@ Below are examples of allowedInteractionsOrder elements specifically for dynamic Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. ```java -builder.withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), - DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), + DeviceLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") )) ``` Example 2: `displayTextAndPIN` Only Description: Use `displayTextAndPIN` interaction only. ```java -builder.withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") +builder.withInteractions(List.of( + DeviceLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") )); ``` Example 3: `confirmationMessage` Only (No Fallback) Description: Insist on `confirmationMessage`; if not available, then fail. ```java -builder.withAllowedInteractionsOrder(List.of( - DynamicLinkInteraction.confirmationMessage("Up to 200 characters of text here..") +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") )); ``` @@ -585,7 +586,6 @@ URI deviceLink = new DeviceLinkBuilder() .withElapsedSeconds(elapsedSeconds) .withLang("eng") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withRelyingPartyName("DEMO") .buildDeviceLink(sessionResponse.getSessionSecret()); ``` @@ -677,13 +677,14 @@ The session status response includes various fields depending on whether the ses * `state`: RUNNING or COMPLETE * `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) * `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. +* `result.details`: Contains additional info when user refused interaction * `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) * `signature`: Contains the following fields based on the signatureProtocol used: - * For `ACSP_V2`: value, serverRandom, signatureAlgorithm, hashAlgorithm - * For `RAW_DIGEST_SIGNATURE`: value, signatureAlgorithm, hashAlgorithm + * For `ACSP_V2`: value, serverRandom, userChallenge, flowType, signatureAlgorithm, signatureAlgorithmParameters, + * For `RAW_DIGEST_SIGNATURE`: value, flowType, signatureAlgorithm, signatureAlgorithmParameters * `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). * `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionFlowUsed`: The interaction flow used for the session. +* `interactionTypeUsed`: The interaction type used for the session. * `deviceIpAddress`: IP address of the mobile device, if requested. ### Examples of querying session status in v3.1 @@ -739,42 +740,53 @@ It's important to validate the session status response to ensure that the return #### Example of validating the authentication sessions response: +##### Authentication response validator setup + +###### Setup TrustedCACertStore + ```java -DyanmicLinkSessionResponse sessionResponse; -// get sessions result -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID(), 10000); +// Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + +// Option 2 - initialize certificate store with custom locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() + .withTrustAnchorTruststorePath("path/to/trustAnchorTruststore.jks") + .withTrustAnchorTruststorePassword("password") + .withIntermediateCAStorePath("path/to/intermediateCAStore.jks") + .withIntermediateCAStorePassword("password") + .build(); -// validate sessions state is completed -if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - // validate sessions status result and map session status to authentication response - AuthenticationResponse response = AuthenticationResponseMapper.from(sessionStatus); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step - - // validate certificate value and signature and map it to authentication identity, uses certificate level QUALIFIED as default. - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.toAuthenticationIdentity(response, "randomChallenge"); -} + +// Option 3 - Provide trust anchors and intermediate CA certificates directly +Set trustAnchors; +List intermediateCACertificates; +TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() + .withTrustAnchors(trustAnchors) + .withIntermediateCACertificates(intermediateCACertificates) + .build(); ``` -##### Authentication response validator setup +###### Setup AuthenticationResponseValidator +```java +TrustedCACertStore trustedCACertStore; +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCACertStore); +``` -````java -// init authentication response validator with trusted certificates -// there are 4 different ways to initialize the validator -// 1. use default values `trusted_certificates.jks` with password `changeit` -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(); +###### Validate sessions status -// 2. provide your own path to truststore and truststore password -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(truststorePath, truststorePassword); +```java +AuthenticationSessionRequest authenticationSessionRequest; +DeviceLinkSessionResponse sessionResponse; -// 3. read trusted certificate yourself and provide it to the validator -X509Certificate[] trustedCertificates = findTrustedCertificates(); -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCertificates); +// get sessions result +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID()); -// 4. init authentication response validator with the empty array and add trusted certificates -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[0]); -X509Certificate certificate = getTrustedCertificate(); -authenticationResponseValidator.addTrustedCACertificate(certificate); -```` +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` #### Example of validating the certificate choice session response: @@ -825,10 +837,15 @@ The session status response may return various error codes indicating the outcom * `WRONG_VC`: User selected the wrong verification code. * `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. * `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. -* `USER_REFUSED_DISPLAYTEXTANDPIN`: User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). -* `USER_REFUSED_VC_CHOICE`: User cancelled verificationCodeChoice screen. -* `USER_REFUSED_CONFIRMATIONMESSAGE`: User cancelled on confirmationMessage screen. -* `USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE`: User cancelled on confirmationMessageAndVerificationCodeChoice screen. +* `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction + was canceled. + * `displayTextAndPIN` - User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). + * `confirmationMessage` - User cancelled on confirmationMessage screen. + * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. +* `PROTOCOL_FAILURE`: An error occurred in the signing protocol. +* `EXPECTED_LINKED_SESSION`: RP has configured signature session that should follow device-link certificate choice session incorrectly and the process + cannot be completed. +* `SERVER_ERROR` - Technical error occurred at the server side and the process was terminated. ## Certificate by document number @@ -858,16 +875,13 @@ RP can directly query the user's signing certificate by document number — no s ```java String documentNumber = "PNOLT-40504040001-MOCK-Q"; -CertificateByDocumentNumberResult certResponse = client - .createCertificateByDocumentNumber +CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() .withDocumentNumber(documentNumber) - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) .getCertificateByDocumentNumber(); -// certResponse.getCertificate(); contains Base64-encoded certificate -// certResponse.getCertificateLevel(); is either ADVANCED or QUALIFIED +// certResponse.certificate(); contains Base64-encoded certificate +// certResponse.certificateLevel(); is either ADVANCED or QUALIFIED ``` ## Notification-based flows @@ -1230,6 +1244,10 @@ Exception Categories These exceptions arise during validation or parsing operations within the library. * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. +* Server side exceptions + * `ProtocolFailureException` Thrown when the Smart-ID API received invalid data such (f.e wrong data in generate device link) + * `ExpectedLinkedSessionException` Thrown when the Relying Party did not configure linked signature session to follow anonymous device-link certificate choice session. + * `SmartIdServerException` Thrown when the Smart-ID terminates the process due to a server-side error. ## Network connection configuration of the client diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index fa1cf8ac..1f0b1b1d 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -41,14 +41,21 @@ public class AuthenticationResponse { private String endResult; private String serverRandom; - private HashType hashType; + private String userChallenge; + private String relyingPartyName; + private SignatureAlgorithm signatureAlgorithm; private String signatureValueInBase64; - private String algorithmName; + private HashAlgorithm hashAlgorithm; private X509Certificate certificate; private AuthenticationCertificateLevel certificateLevel; private String documentNumber; - private String interactionFlowUsed; + private String interactionTypeUsed; + private FlowType flowType; private String deviceIpAddress; + private MaskGenAlgorithm maskGenAlgorithm; + private HashAlgorithm maskHashAlgorithm; + private int saltLength; + private TrailerField trailerField; public String getEndResult() { return endResult; @@ -80,12 +87,12 @@ public byte[] getSignatureValue() { } } - public String getAlgorithmName() { - return algorithmName; + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; } - public void setAlgorithmName(String algorithmName) { - this.algorithmName = algorithmName; + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; } public X509Certificate getCertificate() { @@ -104,14 +111,6 @@ public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) this.certificateLevel = certificateLevel; } - public HashType getHashType() { - return hashType; - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - public String getDocumentNumber() { return documentNumber; } @@ -120,12 +119,12 @@ public void setDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; } - public String getInteractionFlowUsed() { - return interactionFlowUsed; + public String getInteractionTypeUsed() { + return interactionTypeUsed; } - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; } public String getDeviceIpAddress() { @@ -143,4 +142,68 @@ public String getServerRandom() { public void setServerRandom(String serverRandom) { this.serverRandom = serverRandom; } + + public String getUserChallenge() { + return userChallenge; + } + + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + public String getRelyingPartyName() { + return relyingPartyName; + } + + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + public FlowType getFlowType() { + return flowType; + } + + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + + public HashAlgorithm getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + public MaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + public HashAlgorithm getMaskHashAlgorithm() { + return maskHashAlgorithm; + } + + public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { + this.maskHashAlgorithm = maskHashAlgorithm; + } + + public int getSaltLength() { + return saltLength; + } + + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + public TrailerField getTrailerField() { + return trailerField; + } + + public void setTrailerField(TrailerField trailerField) { + this.trailerField = trailerField; + } } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index a2c13b6f..f2a31ee7 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,137 +26,15 @@ * #L% */ -import java.security.cert.X509Certificate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.StringUtil; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; import ee.sk.smartid.rest.dao.SessionStatus; -/** - * Validates and maps the received session status to authentication response - */ -public class AuthenticationResponseMapper { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapper.class); +public interface AuthenticationResponseMapper { /** - * Maps session status to authentication response + * Validates the presence of mandatory fields and maps a SessionStatus to an AuthenticationResponse. * - * @param sessionStatus session status received from Smart-ID server - * @return authentication response + * @param sessionStatus the SessionStatus to map + * @return the mapped AuthenticationResponse */ - public static AuthenticationResponse from(SessionStatus sessionStatus) { - validateSessionStatus(sessionStatus); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate sessionCertificate = sessionStatus.getCert(); - - var authenticationResponse = new AuthenticationResponse(); - authenticationResponse.setEndResult(sessionResult.getEndResult()); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - authenticationResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - authenticationResponse.setCertificate(toCertificate(sessionCertificate)); - authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); - authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - authenticationResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); - authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); - return authenticationResponse; - } - - private static void validateSessionStatus(SessionStatus sessionStatus) { - if (sessionStatus == null) { - throw new SmartIdClientException("Session status parameter is not provided"); - } - - validateResult(sessionStatus.getResult()); - validateSignatureProtocol(sessionStatus); - validateSignature(sessionStatus.getSignature()); - validateCertificate(sessionStatus.getCert()); - - if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { - throw new UnprocessableSmartIdResponseException("Interaction flow used parameter is missing in the session status"); - } - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Session result parameter is missing"); - } - - validateEndResult(sessionResult.getEndResult()); - - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); - } - } - - private static void validateEndResult(String endResult) { - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); - } - if (!"OK".equalsIgnoreCase(endResult)) { - ErrorResultHandler.handle(endResult); - } - } - - private static void validateSignatureProtocol(SessionStatus sessionStatus) { - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - logger.error("Signature protocol parameter is missing in session status"); - throw new UnprocessableSmartIdResponseException("Signature protocol parameter is missing in session status"); - } - - if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { - logger.error("Invalid signature protocol in sessions status: {}", sessionStatus.getSignatureProtocol()); - throw new UnprocessableSmartIdResponseException("Invalid signature protocol in sessions status"); - } - } - - private static void validateSignature(SessionSignature sessionSignature) { - if (sessionSignature == null) { - throw new UnprocessableSmartIdResponseException("Signature parameter is missing in session status"); - } - - if (StringUtil.isEmpty(sessionSignature.getValue())) { - throw new UnprocessableSmartIdResponseException("Value parameter is missing in signature"); - } - - if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { - throw new UnprocessableSmartIdResponseException("Server random parameter is missing in signature"); - } - - if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature algorithm parameter is missing in signature"); - } - } - - private static void validateCertificate(SessionCertificate sessionCertificate) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); - } - - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); - } - - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); - } - } - - private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { - return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - } - - private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { - return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - } + AuthenticationResponse from(SessionStatus sessionStatus); } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index 7b5b4d9f..36ee6155 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -26,36 +26,39 @@ * #L% */ -import static java.util.Arrays.asList; import static org.slf4j.LoggerFactory.getLogger; -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.KeyStoreException; +import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.Signature; -import java.security.cert.CertificateException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateParsingException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Enumeration; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.Base64; import java.util.List; -import java.util.Objects; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import javax.security.auth.x500.X500Principal; +import java.util.Set; +import org.bouncycastle.asn1.x500.style.BCStyle; import org.slf4j.Logger; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.CertificateAttributeUtil; import ee.sk.smartid.util.StringUtil; /** @@ -65,118 +68,106 @@ public class AuthenticationResponseValidator { private static final Logger logger = getLogger(AuthenticationResponseValidator.class); - private final List trustedCACertificates = new ArrayList<>(); - - /** - * Initializes the mapper with trusted CA certificates from a keystore. - *

    - * Uses default values to initialize the keystore. - */ - public AuthenticationResponseValidator() { - initializeTrustedCACertificatesFromKeyStore("/trusted_certificates.jks", "changeit"); - } + private static final Set ALLOWED_AUTHENTICATION_EXTENDED_KEY_USAGE = Set.of("1.3.6.1.5.5.7.3.2", "1.3.6.1.4.1.62306.5.7.0"); + private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; + private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; + private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; - /** - * Initializes the mapper with trusted CA certificates from a keystore. - * - * @param truststorePath path to the keystore - * @param truststorePassword password for the keystore - */ - public AuthenticationResponseValidator(String truststorePath, String truststorePassword) { - initializeTrustedCACertificatesFromKeyStore(truststorePath, truststorePassword); - } + private final TrustedCACertStore trustedCaCertStore; + private final AuthenticationResponseMapper authenticationResponseMapper; /** - * Initializes the mapper with trusted CA certificates from the input + * Initializes the validator with a {@link TrustedCACertStore}. * - * @param trustedCertificates trusted CA certificates + * @param trustedCaCertStore the store containing trusted CA certificates */ - public AuthenticationResponseValidator(X509Certificate[] trustedCertificates) { - trustedCACertificates.addAll(asList(trustedCertificates)); + public AuthenticationResponseValidator(TrustedCACertStore trustedCaCertStore) { + this(trustedCaCertStore, DefaultAuthenticationResponseMapper.getInstance()); } /** - * Adds a trusted CA certificate to the mapper + * Initializes the validator with a {@link TrustedCACertStore} and a custom {@link AuthenticationResponseMapper}. * - * @param certificate trusted CA certificate + * @param trustedCaCertStore the store containing trusted CA certificates + * @param authenticationResponseMapper the mapper to convert session status to authentication response */ - public void addTrustedCACertificate(X509Certificate certificate) { - trustedCACertificates.add(certificate); + public AuthenticationResponseValidator(TrustedCACertStore trustedCaCertStore, AuthenticationResponseMapper authenticationResponseMapper) { + this.trustedCaCertStore = trustedCaCertStore; + this.authenticationResponseMapper = authenticationResponseMapper; } /** - * Maps the Smart-ID authentication response {@link AuthenticationResponse} to {@link AuthenticationIdentity} + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. *

    - * Uses {@link AuthenticationCertificateLevel#QUALIFIED} as the request certificate level + * This method sets brokeredRpName value to null * - * @param authenticationResponse Smart-ID authentication response - * @return authentication identity + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name + * @return the authentication identity */ - public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, String rpChallenge) { - return toAuthenticationIdentity(authenticationResponse, AuthenticationCertificateLevel.QUALIFIED, rpChallenge); + public AuthenticationIdentity validate(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); } /** - * Maps the Smart-ID authentication response {@link AuthenticationResponse} to {@link AuthenticationIdentity} + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. * - * @param authenticationResponse Smart-ID authentication response - * @param requestedCertificateLevel Certificate level used in the authentication session request - * @param rpChallenge Generate string used in the authentication session request - * @return authentication identity + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name + * @param brokeredRpName the brokered relying party name + * @return the authentication identity */ - public AuthenticationIdentity toAuthenticationIdentity(AuthenticationResponse authenticationResponse, - AuthenticationCertificateLevel requestedCertificateLevel, - String rpChallenge) { - validateInputs(authenticationResponse, rpChallenge); - validateCertificate(authenticationResponse, requestedCertificateLevel); - validateSignature(authenticationResponse, rpChallenge); + public AuthenticationIdentity validate(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel())); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); } - private void validateInputs(AuthenticationResponse authenticationResponse, String rpChallenge) { - if (authenticationResponse == null) { - throw new SmartIdClientException("Device link authentication response is not provided"); + private static void validateInputs(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("`sessionStatus` is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("`authenticationSessionRequest` is not provided"); } - if (StringUtil.isEmpty(rpChallenge)) { - throw new SmartIdClientException("RP challenge is not provided"); + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("`schemaName` is not provided"); } } private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (authenticationResponse.getCertificate() == null) { - throw new SmartIdClientException("Certificate is not provided"); - } - validateCertificateNotExpired(authenticationResponse.getCertificate()); - validateCertificateIsTrusted(authenticationResponse.getCertificate()); + validateCertificateIsCurrentlyValid(authenticationResponse.getCertificate()); + validateCertificateChain(authenticationResponse); + validateCertificatePurpose(authenticationResponse); validateCertificateLevel(authenticationResponse, requestedCertificateLevel); } - private void validateSignature(AuthenticationResponse authenticationResponse, String rpChallenge) { - if (StringUtil.isEmpty(authenticationResponse.getAlgorithmName())) { - throw new SmartIdClientException("Algorithm name is not provided"); - } - if (StringUtil.isEmpty(authenticationResponse.getSignatureValueInBase64())) { - throw new SmartIdClientException("Signature value is not provided"); - } + private void validateCertificatePurpose(AuthenticationResponse authenticationResponse) { + X509Certificate certificate = authenticationResponse.getCertificate(); try { - Signature signature = getSignature(authenticationResponse); - signature.initVerify(authenticationResponse.getCertificate().getPublicKey()); - String data = createSignatureData(authenticationResponse, rpChallenge); - signature.update(data.getBytes(StandardCharsets.UTF_8)); - byte[] signedHash = authenticationResponse.getSignatureValue(); - if (!signature.verify(signedHash)) { - throw new UnprocessableSmartIdResponseException("Failed to verify validity of signature returned by Smart-ID"); + List extendedKeyUsage = certificate.getExtendedKeyUsage(); + if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(ALLOWED_AUTHENTICATION_EXTENDED_KEY_USAGE::contains)) { + logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); + throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); } - } catch (GeneralSecurityException ex) { - logger.error("Signature verification failed"); - throw new UnprocessableSmartIdResponseException("Signature verification failed", ex); + + boolean[] keyUsage = certificate.getKeyUsage(); + if (keyUsage == null + || !(keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] + || keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); + } + } catch (CertificateParsingException ex) { + throw new UnprocessableSmartIdResponseException("Authentication certificate is incorrect", ex); } } private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (requestedCertificateLevel == null) { - return; - } if (authenticationResponse.getCertificateLevel() == null) { throw new SmartIdClientException("Certificate level is not provided"); } @@ -185,113 +176,99 @@ private void validateCertificateLevel(AuthenticationResponse authenticationRespo } } - private void validateCertificateIsTrusted(X509Certificate responseCertificate) { - CertDnDetails issuerDn = CertDnDetails.from(responseCertificate.getIssuerX500Principal()); - - for (X509Certificate trustedCACertificate : trustedCACertificates) { - logger.debug("Verifying signer's certificate '{}' against CA certificate '{}'", - responseCertificate.getSubjectX500Principal(), - trustedCACertificate.getSubjectX500Principal()); - - CertDnDetails caCertDn = CertDnDetails.from(trustedCACertificate.getSubjectX500Principal()); - - if (!CertDnDetails.equal(issuerDn, caCertDn)) { - logger.debug("Skipped trusted CA certificate '{}', no match with signer's certificate issuer '{}'", - trustedCACertificate.getSubjectX500Principal(), - responseCertificate.getIssuerX500Principal()); - continue; - } - - try { - responseCertificate.verify(trustedCACertificate.getPublicKey()); - logger.info("Certificate verification passed for '{}' against CA certificate '{}'", - responseCertificate.getSubjectX500Principal(), - trustedCACertificate.getSubjectX500Principal()); - return; - } catch (GeneralSecurityException ex) { - logger.debug("Error verifying signer's certificate: {} against CA certificate: {}", - responseCertificate.getSubjectX500Principal(), - trustedCACertificate.getSubjectX500Principal(), ex); + private void validateSignature(AuthenticationResponse authenticationResponse, + AuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + try { + Signature result = getSignature(authenticationResponse); + result.initVerify(authenticationResponse.getCertificate().getPublicKey()); + result.update(constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName)); + byte[] signedHash = authenticationResponse.getSignatureValue(); + if (!result.verify(signedHash)) { + logger.error("Signature value does not match the calculated signature for authentication response"); + throw new UnprocessableSmartIdResponseException("Failed to verify validity of authentication signature returned by Smart-ID"); } + } catch (GeneralSecurityException ex) { + throw new UnprocessableSmartIdResponseException("Authentication signature validation failed", ex); } - - logger.error("No suitable trusted CA certificate found: '{}'. Ensure that this CA certificate is present in the trusted CA certificate list", - responseCertificate.getIssuerX500Principal()); - throw new UnprocessableSmartIdResponseException("Signer's certificate is not trusted"); } - private void initializeTrustedCACertificatesFromKeyStore(String truststorePath, String truststorePassword) { - try (InputStream is = AuthenticationResponseValidator.class.getResourceAsStream(truststorePath)) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, truststorePassword.toCharArray()); - Enumeration aliases = keystore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - addTrustedCACertificate(certificate); - } - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing trusted CA certificates", e); - throw new SmartIdClientException("Error initializing trusted CA certificates", e); - } + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + AuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + Base64.getEncoder().encodeToString(authenticationSessionRequest.relyingPartyName().getBytes(StandardCharsets.UTF_8)), + StringUtil.isEmpty(brokeredRpName) ? "" : Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(calculateInteractionsDigest(authenticationSessionRequest)), + authenticationResponse.getInteractionTypeUsed(), + StringUtil.orEmpty(authenticationSessionRequest.initialCallbackURL()), + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); } - private static Signature getSignature(AuthenticationResponse authenticationResponse) throws NoSuchAlgorithmException { - String algorithm = authenticationResponse.getAlgorithmName().replace("Encryption", ""); + private void validateCertificateChain(AuthenticationResponse authenticationResponse) { try { - return Signature.getInstance(algorithm); - } catch (NoSuchAlgorithmException ex) { - logger.error("Invalid signature algorithm was provided: {}", algorithm); - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ + setCertificate(authenticationResponse.getCertificate()); + }}); + CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); + params.addCertStore(intermediateStore); + params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); + + if (logger.isDebugEnabled()) { + X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); + X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); + X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", + CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); + } + } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { + throw new UnprocessableSmartIdResponseException("Authentication certificate chain validation failed", ex); } } - private static void validateCertificateNotExpired(X509Certificate certificate) { + private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { try { certificate.checkValidity(); } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - throw new UnprocessableSmartIdResponseException("Signer's certificate is not valid", ex); + logger.error("Authentication certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Authentication certificate is invalid", ex); } } - private static String createSignatureData(AuthenticationResponse authenticationResponse, String rpChallenge) { - return String.format("%s;%s;%s", SignatureProtocol.ACSP_V2.name(), - authenticationResponse.getServerRandom(), - rpChallenge); - } - - private record CertDnDetails(String country, String organization, String commonName) { - - private static CertDnDetails from(X500Principal principal) { - String country = null; - String organization = null; - String commonName = null; - - LdapName ldapName; - try { - ldapName = new LdapName(principal.getName()); - } catch (InvalidNameException e) { - String errorMessage = "Error getting certificate distinguished name"; - logger.error(errorMessage, e); - throw new SmartIdClientException(errorMessage, e); - } - - for (Rdn rdn : ldapName.getRdns()) { - if ("C".equalsIgnoreCase(rdn.getType())) { - country = rdn.getValue().toString(); - } else if ("O".equalsIgnoreCase(rdn.getType())) { - organization = rdn.getValue().toString(); - } else if ("CN".equalsIgnoreCase(rdn.getType())) { - commonName = rdn.getValue().toString(); - } - } - return new CertDnDetails(country, organization, commonName); + private static Signature getSignature(AuthenticationResponse authenticationResponse) { + try { + var params = new PSSParameterSpec(authenticationResponse.getHashAlgorithm().getAlgorithmName(), + authenticationResponse.getMaskGenAlgorithm().getMgfName(), + new MGF1ParameterSpec(authenticationResponse.getMaskHashAlgorithm().getAlgorithmName()), + authenticationResponse.getSaltLength(), + authenticationResponse.getTrailerField().getPssSpecValue()); + var signature = Signature.getInstance(authenticationResponse.getSignatureAlgorithm().getAlgorithmName()); + signature.setParameter(params); + return signature; + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", authenticationResponse.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } catch (InvalidAlgorithmParameterException ex) { + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); } + } - private static boolean equal(CertDnDetails first, CertDnDetails second) { - return Objects.equals(first.country, second.country) && - Objects.equals(first.organization, second.organization) && - Objects.equals(first.commonName, second.commonName); - } + private static byte[] calculateInteractionsDigest(AuthenticationSessionRequest authenticationSessionRequest) { + return DigestCalculator.calculateDigest(authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8), HashType.SHA256); } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java index 72c4a6a6..c2488519 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java @@ -33,10 +33,10 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.dao.SessionCertificate; import ee.sk.smartid.rest.dao.SessionResult; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; /** * Validates and maps the received session status to certificate choice response @@ -71,7 +71,7 @@ public static CertificateChoiceResponse from(SessionStatus sessionStatus, Certif certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); certificateChoiceResponse.setCertificate(certificate); certificateChoiceResponse.setCertificateLevel(CertificateLevel.valueOf(sessionStatus.getCert().getCertificateLevel())); - certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); return certificateChoiceResponse; } @@ -87,18 +87,15 @@ private static void validateResult(SessionResult sessionResult) { if (sessionResult == null) { throw new UnprocessableSmartIdResponseException("Session result parameter is missing"); } - validateEndResult(sessionResult.getEndResult()); - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); - } - } - - private static void validateEndResult(String endResult) { + String endResult = sessionResult.getEndResult(); if (StringUtil.isEmpty(endResult)) { throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); } if (!"OK".equalsIgnoreCase(endResult)) { - ErrorResultHandler.handle(endResult); + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); } } diff --git a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java new file mode 100644 index 00000000..ca486453 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java @@ -0,0 +1,286 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to authentication response + */ +public class DefaultAuthenticationResponseMapper implements AuthenticationResponseMapper { + + private static final Logger logger = LoggerFactory.getLogger(DefaultAuthenticationResponseMapper.class); + + private static AuthenticationResponseMapper instance; + + private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; + private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; + private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; + + public static AuthenticationResponseMapper getInstance() { + if (instance == null) { + instance = new DefaultAuthenticationResponseMapper(); + } + return instance; + } + + /** + * Maps session status to authentication response + * + * @param sessionStatus session status received from Smart-ID server + * @return authentication response + */ + @Override + public AuthenticationResponse from(SessionStatus sessionStatus) { + validateSessionStatus(sessionStatus); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + + var authenticationResponse = new AuthenticationResponse(); + authenticationResponse.setEndResult(sessionResult.getEndResult()); + authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); + authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); + authenticationResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); + + authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()).orElse(null); + authenticationResponse.setSignatureAlgorithm(signatureAlgorithm); + + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null); + authenticationResponse.setHashAlgorithm(hashAlgorithm); + MaskGenAlgorithm maskGenAlgorithm = MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm()).orElse(null); + authenticationResponse.setMaskGenAlgorithm(maskGenAlgorithm); + var maskGenHashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null); + authenticationResponse.setMaskHashAlgorithm(maskGenHashAlgorithm); + authenticationResponse.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + TrailerField trailerField = TrailerField.fromString(signatureAlgorithmParameters.getTrailerField()).orElse(null); + authenticationResponse.setTrailerField(trailerField); + + authenticationResponse.setCertificate(toCertificate(sessionCertificate)); + authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); + + authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); + authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return authenticationResponse; + } + + private static void validateSessionStatus(SessionStatus sessionStatus) { + if (sessionStatus == null) { + throw new SmartIdClientException("Input parameter `sessionsStatus` is not provided"); + } + + validateResult(sessionStatus.getResult()); + validateSignatureProtocol(sessionStatus); + validateSignature(sessionStatus.getSignature()); + validateCertificate(sessionStatus.getCert()); + + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("Session status field `interactionTypeUsed` is empty"); + } + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Session status field `result` is empty"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Session status field `result.endResult` is empty"); + } + if (!"OK".equals(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Session status field `result.documentNumber` is empty"); + } + } + + private static void validateSignatureProtocol(SessionStatus sessionStatus) { + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Session status field `signatureProtocol` is empty"); + } + + if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { + logger.error("Invalid `signatureProtocol` in authentication sessions status: {}", sessionStatus.getSignatureProtocol()); + throw new UnprocessableSmartIdResponseException("Invalid `signatureProtocol` in sessions status"); + } + } + + private static void validateSignature(SessionSignature sessionSignature) { + if (sessionSignature == null) { + throw new UnprocessableSmartIdResponseException("Session status field `signature` is missing"); + } + + if (StringUtil.isEmpty(sessionSignature.getValue())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.value` is empty"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { + logger.error("Session status field `signature.value` is not in Base64-encoded format: {}", sessionSignature.getValue()); + throw new UnprocessableSmartIdResponseException("Session status field `signature.value` is not in Base64-encoded format"); + } + + if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.severRandom` is empty"); + } + int serverRandomLength = sessionSignature.getServerRandom().length(); + if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { + logger.error("Signature field `serverRandom` is less than required length: expected {} < {}", serverRandomLength, MINIMUM_SERVER_RANDOM_LENGTH); + throw new UnprocessableSmartIdResponseException("Session status field `signature.serverRandom` is less than required length"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { + logger.error("Session status field `signature.serverRandom` is not in Base64-encoded format: {}", sessionSignature.getServerRandom()); + throw new UnprocessableSmartIdResponseException("Session status field `signature.serverRandom` is not in Base64-encoded format"); + } + + if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.userChallenge` is empty"); + } + if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { + logger.error("`signature.userChallenge` value in session status is not in the expected Base64-encoded format: {}", sessionSignature.getUserChallenge()); + throw new UnprocessableSmartIdResponseException("`signature.userChallenge` value in session status is not in the expected Base64-encoded format"); + } + + if (StringUtil.isEmpty(sessionSignature.getFlowType())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.flowType` is empty"); + } + if (!FlowType.isSupported(sessionSignature.getFlowType())) { + logger.error("Invalid `signature.flowType` in session status: {}", sessionSignature.getFlowType()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.flowType` in session status"); + } + + if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithm` is empty"); + } + Optional signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); + if (signatureAlgorithm.isEmpty()) { + logger.error("Invalid `signature.signatureAlgorithm` in the session status: {}", sessionSignature.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithm` in the session status"); + } + + validateSignatureAlgorithmParameters(sessionSignature); + } + + private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + if (sessionSignature.getSignatureAlgorithmParameters() == null) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters` is missing"); + } + if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.hashAlgorithm` is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status: {}", signatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status"); + } + + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm` is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm` is empty"); + } + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status"); + } + + Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (maskGenHashAlgorithm.isEmpty()) { + logger.error("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status: {}", + maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in empty"); + } + if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { + logger.error("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`: expected {}, got {}", + hashAlgorithm.get().getAlgorithmName(), + maskGenHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`"); + } + + if (signatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.saltLength` is empty"); + } + int octetLength = hashAlgorithm.get().getOctetLength(); + if (octetLength != signatureAlgorithmParameters.getSaltLength()) { + logger.error("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status: expected {}, got {}", + octetLength, + signatureAlgorithmParameters.getSaltLength()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status"); + } + + if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + } + if (!TrailerField.OXBC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { + logger.error("Invalid `signature.signatureAlgorithmParameters.trailerField` in session status: {}", signatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status"); + } + } + + private static void validateCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); + } + + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); + } + } + + private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { + return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + } + + private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { + return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java new file mode 100644 index 00000000..10ad8eaa --- /dev/null +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java @@ -0,0 +1,86 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Implementation of the TrustedCAStore that manages a collection of trusted CA certificates. + */ +public class DefaultTrustedCACertStore implements TrustedCACertStore { + + private final Set trustAnchors = new HashSet<>(); + private final List trustedCACertificates = new ArrayList<>(); + private final boolean ocspEnabled; + + /** + * Initializes the trusted CA certificates from an array of X509 certificates. + * + * @param trustAnchors a set of TrustAnchor objects representing the trust anchors + * @param trustedCaCertificates a list of X509Certificate objects representing the trusted CA certificates + * @param ocspEnabled flag to disable or active OCSP validations + * + * @throws SmartIdClientException if the provided array is null or empty + */ + + public DefaultTrustedCACertStore(Set trustAnchors, List trustedCaCertificates, boolean ocspEnabled) { + this.trustAnchors.addAll(trustAnchors); + trustedCACertificates.addAll(trustedCaCertificates); + this.ocspEnabled = ocspEnabled; + } + + @Override + public List getTrustedCACertificates() { + return List.copyOf(trustedCACertificates); + } + + @Override + public Set getTrustAnchors() { + return Set.copyOf(trustAnchors); + } + + @Override + public boolean isOcspEnabled() { + return ocspEnabled; + } + + interface Builder { + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instance + */ + TrustedCACertStore build(); + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java new file mode 100644 index 00000000..4244bd54 --- /dev/null +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java @@ -0,0 +1,158 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertStore; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Builder for creating a DefaultTrustedCACertStore instance. + * This builder allows setting trust anchors, trusted CA certificates, and OCSP validation settings. + */ +public class DefaultTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(DefaultTrustedCAStoreBuilder.class); + + private Set trustAnchors; + private List intermediateCACertificates; + private boolean ocspEnabled = true; + private X509Certificate ocspValidationCert; + + /** + * Sets the trust anchors for the TrustedCAStore. + * + * @param trustAnchors a set of TrustAnchor objects to be used as trust anchors + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withTrustAnchors(Set trustAnchors) { + this.trustAnchors = trustAnchors; + return this; + } + + /** + * Sets the trusted CA certificates for the TrustedCAStore. + * + * @param intermediateCACertificates a list of X509Certificate objects to be used as trusted CA certificates + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withIntermediateCACertificate(List intermediateCACertificates) { + this.intermediateCACertificates = List.copyOf(intermediateCACertificates); + return this; + } + + /** + * Sets whether OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @param enabled true to enable OCSP validation, false to disable it + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Sets the certificate used for OCSP validation. + * + * @param ocspValidationCert the X509Certificate to be used for OCSP validation + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOCSPValidationCert(X509Certificate ocspValidationCert) { + this.ocspValidationCert = ocspValidationCert; + return this; + } + + @Override + public DefaultTrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("Does not work yet, will be implemented later"); + } + validateTrustAnchors(); + validateIntermediateCaCertificates(); + return new DefaultTrustedCACertStore(Set.copyOf(trustAnchors), List.copyOf(intermediateCACertificates), ocspEnabled); + } + + private void validateTrustAnchors() { + for (TrustAnchor trustAnchor : trustAnchors) { + try { + trustAnchor.getTrustedCert().verify(trustAnchor.getTrustedCert().getPublicKey()); + } catch (GeneralSecurityException e) { + throw new SmartIdClientException("", e); + } + } + } + + private void validateIntermediateCaCertificates() { + for (X509Certificate cert : intermediateCACertificates) { + validateIntermediateCACertificate(cert); + } + } + + private void validateIntermediateCACertificate(X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + if (ocspEnabled) { + var certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(ocspValidationCert))); + pkixParameters.setCertStores(List.of(certStore)); + } + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.error("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index 353f312a..80392cdc 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -71,6 +71,8 @@ public class DeviceLinkAuthenticationSessionRequestBuilder { private String documentNumber; private String initialCallbackURL; + private AuthenticationSessionRequest authenticationSessionRequest; + /** * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector * @@ -239,7 +241,7 @@ public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackURL(Stri *

* * @return init session response - * @throws SmartIdClientException if request parameters are invalid + * @throws SmartIdClientException if request parameters are invalid * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initAuthenticationSession() { @@ -247,9 +249,23 @@ public DeviceLinkSessionResponse initAuthenticationSession() { AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(deviceLinkAuthenticationSessionResponse); + this.authenticationSessionRequest = authenticationRequest; return deviceLinkAuthenticationSessionResponse; } + /** + * Returns the authentication session request created during the initialization + * + * @return the authentication session request + * @throws SmartIdClientException when session is not yet initialized and method is called + */ + public AuthenticationSessionRequest getAuthenticationSessionRequest() { + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Authentication session request has not been initialized yet"); + } + return authenticationSessionRequest; + } + private DeviceLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null && documentNumber != null) { logger.error("Both semanticsIdentifier and documentNumber are set – only one can be used"); @@ -319,32 +335,21 @@ private void validateInitialCallbackURL() { } private AuthenticationSessionRequest createAuthenticationRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); - signatureProtocolParameters.setRpChallenge(rpChallenge); - signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - - var signatureAlgorithmParameters = new SignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(this.hashAlgorithm.getValue()); - signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - - request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setInteractions(DeviceLinkUtil.encodeToBase64(interactions)); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - request.setCapabilities(capabilities); - request.setInitialCallbackURL(initialCallbackURL); - return request; + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(this.hashAlgorithm.getValue())); + + return new AuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + DeviceLinkUtil.encodeToBase64(interactions), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + initialCallbackURL + ); } private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 3fe8d12a..f018686c 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -34,19 +34,19 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.HashAlgorithm; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.DeviceLinkUtil; -import ee.sk.smartid.util.SignatureUtil; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.SignatureUtil; +import ee.sk.smartid.util.StringUtil; public class DeviceLinkSignatureSessionRequestBuilder { @@ -253,8 +253,8 @@ public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackURL(String in * * @return a {@link DeviceLinkSessionResponse} containing session details such as * session ID, session token, session secret and device link base URL. - * @throws SmartIdClientException if request parameters are invalid - * @throws UnprocessableSmartIdResponseException if the response is missing required fields + * @throws SmartIdClientException if request parameters are invalid + * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initSignatureSession() { validateParameters(); @@ -289,8 +289,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { } signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - var signatureAlgorithmParameters = new SignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm.getValue()); + var signatureAlgorithmParameters = new SignatureAlgorithmParameters(hashAlgorithm.getValue()); signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); request.setSignatureProtocolParameters(signatureProtocolParameters); @@ -299,8 +298,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { request.setInteractions(DeviceLinkUtil.encodeToBase64(interactions)); if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + var requestProperties = new RequestProperties(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } request.setCapabilities(capabilities); diff --git a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java index 921ef76b..c6f131e1 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java @@ -173,9 +173,8 @@ private CertificateChoiceSessionRequest createCertificateRequest() { request.setNonce(nonce); request.setCapabilities(capabilities); - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - if (requestProperties.hasProperties()) { + if (this.shareMdClientIpAddress != null) { + var requestProperties = new RequestProperties(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index 9dee57a1..4868677f 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -26,6 +26,9 @@ * #L% */ +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; @@ -38,29 +41,43 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.util.StringUtil; /** * Handles session status results that end as completed but with an error */ public class ErrorResultHandler { - public static void handle(String endResult) { - if (endResult == null) { + public static void handle(SessionResult sessionResult) { + if (sessionResult == null) { throw new SmartIdClientException("Session end result is not provided"); } - - switch (endResult.toUpperCase()) { + switch (sessionResult.getEndResult()) { case "USER_REFUSED" -> throw new UserRefusedException(); case "TIMEOUT" -> throw new SessionTimeoutException(); case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); - case "USER_REFUSED_DISPLAYTEXTANDPIN" -> throw new UserRefusedDisplayTextAndPinException(); - case "USER_REFUSED_VC_CHOICE" -> throw new UserRefusedVerificationChoiceException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE" -> throw new UserRefusedConfirmationMessageException(); - case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); - default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + endResult); + case "USER_REFUSED_INTERACTION" -> handleUserRefusedInteraction(sessionResult); + case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); + case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); + case "SERVER_ERROR" -> throw new SmartIdServerException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); + } + } + + private static void handleUserRefusedInteraction(SessionResult sessionResult) { + if (sessionResult.getDetails() == null || StringUtil.isEmpty(sessionResult.getDetails().getInteraction())) { + throw new UnprocessableSmartIdResponseException("Details for refused interaction are missing"); + } + switch (sessionResult.getDetails().getInteraction()) { + case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); + case "verificationCodeChoice" -> throw new UserRefusedVerificationChoiceException(); + case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); + case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); } } } diff --git a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java new file mode 100644 index 00000000..d9a89d35 --- /dev/null +++ b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java @@ -0,0 +1,219 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.StringUtil; + +public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); + + private String trustAnchorTruststorePath = "/sid_trust_anchor_certificates.jks"; + private String trustAnchorTruststorePassword = "changeit"; + + private String intermediateCATruststorePath = "/trusted_certificates.jks"; + private String trustedCaTruststorePassword = "changeit"; + + private boolean ocspEnabled = false; // TODO - 03.07.25: set to true if OCSP validations is working + private X509Certificate ocspValidationCert; // TODO - 02.07.25: implement reading from a file system + + /** + * Sets the path to the trust anchor keystore file. + * + * @param path the path to the trust anchor keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePath(String path) { + this.trustAnchorTruststorePath = path; + return this; + } + + /** + * Sets the password for the trust anchor keystore. + * + * @param password the password for the trust anchor keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePassword(String password) { + this.trustAnchorTruststorePassword = password; + return this; + } + + /** + * Sets the path to the intermediate CA keystore file. + * + * @param path the path to the trusted CA keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePath(String path) { + this.intermediateCATruststorePath = path; + return this; + } + + /** + * Sets the password for the trusted CA keystore. + * + * @param password the password for the trusted CA keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePassword(String password) { + this.trustedCaTruststorePassword = password; + return this; + } + + /** + * Enables or disables OCSP (Online Certificate Status Protocol) for certificate validation. + * + * @param enabled true to enable OCSP, false to disable it + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instances + */ + @Override + public TrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("OCSP validation does not work yet, will be implemented later"); + } + Set trustAnchors = loadTrustAnchors(); + List trustedCACertificates = loadValidatedIntermediateCACertificates(trustAnchors); + return new DefaultTrustedCACertStore(trustAnchors, trustedCACertificates, ocspEnabled); + } + + private Set loadTrustAnchors() { + if (StringUtil.isEmpty(trustAnchorTruststorePath)) { + throw new SmartIdClientException("Trust anchor truststore path must be set"); + } + if (StringUtil.isEmpty(trustAnchorTruststorePassword)) { + throw new SmartIdClientException("Trust anchor truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(trustAnchorTruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustAnchorTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + Set trustAnchors = new HashSet<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.verify(certificate.getPublicKey()); + certificate.checkValidity(); + trustAnchors.add(new TrustAnchor(certificate, null)); + } + return trustAnchors; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing trust anchor certificate", e); + throw new SmartIdClientException("Error initializing trust anchor certificate", e); + } catch (SignatureException | InvalidKeyException | NoSuchProviderException ex) { + throw new SmartIdClientException("Failed to verify trust anchor certificate", ex); + } + } + + private List loadValidatedIntermediateCACertificates(Set trustAnchors) { + if (StringUtil.isEmpty(intermediateCATruststorePath)) { + throw new SmartIdClientException("Intermediate CA certificate truststore path must be set"); + } + if (StringUtil.isEmpty(trustedCaTruststorePassword)) { + throw new SmartIdClientException("Intermediate CA certificate truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(intermediateCATruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustedCaTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + List trustedCACertificates = new ArrayList<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.checkValidity(); + validateCertificate(trustAnchors, certificate); + trustedCACertificates.add(certificate); + } + return trustedCACertificates; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing intermediate CA certificates", e); + throw new SmartIdClientException("Error initializing intermediate CA certificates", e); + } + } + + private void validateCertificate(Set trustAnchors, X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.debug("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/FlowType.java b/src/main/java/ee/sk/smartid/FlowType.java new file mode 100644 index 00000000..c487e6a6 --- /dev/null +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -0,0 +1,51 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +public enum FlowType { + + QR("QR"), + WEB2APP("Web2App"), + APP2APP("App2App"), + NOTIFICATION("Notification"); + + private final String description; + + FlowType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + public static boolean isSupported(String input) { + return Arrays.stream(FlowType.values()).anyMatch(flowType -> flowType.getDescription().equals(input)); + } +} diff --git a/src/main/java/ee/sk/smartid/HashAlgorithm.java b/src/main/java/ee/sk/smartid/HashAlgorithm.java new file mode 100644 index 00000000..173a9ffb --- /dev/null +++ b/src/main/java/ee/sk/smartid/HashAlgorithm.java @@ -0,0 +1,62 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Optional; + +public enum HashAlgorithm { + + SHA_256("SHA-256", 32), + SHA_384("SHA-384", 48), + SHA_512("SHA-512", 64), + SHA3_256("SHA3-256", 32), + SHA3_384("SHA3-384", 48), + SHA3_512("SHA3-512", 64); + + private final String algorithmName; + private final int octetLength; + + HashAlgorithm(String algorithmName, int octetLength) { + this.algorithmName = algorithmName; + this.octetLength = octetLength; + } + + public String getAlgorithmName() { + return algorithmName; + } + + public int getOctetLength() { + return octetLength; + } + + public static Optional fromString(String input) { + return Arrays.stream(HashAlgorithm.values()) + .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) + .findFirst(); + } +} diff --git a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java new file mode 100644 index 00000000..4be8732d --- /dev/null +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -0,0 +1,60 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents mask algorithm in the response and the value used in recrating the signature. + */ +public enum MaskGenAlgorithm { + + ID_MGF1("id-mgf1", "MGF1"); + + private final String algorithmName; + private final String mgfName; + + MaskGenAlgorithm(String algorithmName, String mgfName) { + this.algorithmName = algorithmName; + this.mgfName = mgfName; + } + + public String getAlgorithmName() { + return algorithmName; + } + + public String getMgfName() { + return mgfName; + } + + public static Optional fromString(String input) { + return Arrays.stream(MaskGenAlgorithm.values()) + .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) + .findFirst(); + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index 7f857d2b..1ba08fef 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -38,8 +38,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; @@ -47,7 +45,10 @@ import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.StringUtil; /** * Class for building a notification authentication session request @@ -61,7 +62,7 @@ public class NotificationAuthenticationSessionRequestBuilder { private String relyingPartyUUID; private String relyingPartyName; private AuthenticationCertificateLevel certificateLevel; - private String randomChallenge; + private String rpChallenge; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; private String nonce; private List interactions; @@ -121,7 +122,7 @@ public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(Auth * @return this builder */ public NotificationAuthenticationSessionRequestBuilder withRandomChallenge(String randomChallenge) { - this.randomChallenge = randomChallenge; + this.rpChallenge = randomChallenge; return this; } @@ -250,7 +251,7 @@ private void validateRequestParameters() { } private void validateSignatureParameters() { - if (StringUtil.isEmpty(randomChallenge)) { + if (StringUtil.isEmpty(rpChallenge)) { logger.error("Parameter randomChallenge must be set"); throw new SmartIdClientException("Parameter randomChallenge must be set"); } @@ -268,7 +269,7 @@ private void validateSignatureParameters() { private byte[] getDecodedRandomChallenge() { Base64.Decoder decoder = Base64.getDecoder(); try { - return decoder.decode(randomChallenge); + return decoder.decode(rpChallenge); } catch (IllegalArgumentException e) { logger.error("Parameter randomChallenge is not a valid Base64 encoded string"); throw new SmartIdClientException("Parameter randomChallenge is not a valid Base64 encoded string"); @@ -298,27 +299,21 @@ private void validateAllowedInteractionOrder() { } private AuthenticationSessionRequest createAuthenticationRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); - signatureProtocolParameters.setRpChallenge(randomChallenge); - signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setInteractions(encodeInteractionsToBase64(interactions)); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - request.setCapabilities(capabilities); - return request; + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters("SHA-512")); + + return new AuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + encodeInteractionsToBase64(interactions), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + nonce + ); } private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 2b82bd7f..606be819 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -190,8 +190,7 @@ private CertificateChoiceSessionRequest createCertificateChoiceRequest() { request.setNonce(nonce); if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + var requestProperties = new RequestProperties(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 904ed8c5..3b149975 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -34,19 +34,18 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.DeviceLinkUtil; -import ee.sk.smartid.util.NotificationUtil; -import ee.sk.smartid.util.SignatureUtil; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.NotificationUtil; +import ee.sk.smartid.util.SignatureUtil; +import ee.sk.smartid.util.StringUtil; public class NotificationSignatureSessionRequestBuilder { @@ -263,8 +262,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { request.setInteractions(NotificationUtil.encodeToBase64(allowedInteractionsOrder)); if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(this.shareMdClientIpAddress); + var requestProperties = new RequestProperties(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index 4621070a..ca894220 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,6 +26,9 @@ * #L% */ +import java.util.Arrays; +import java.util.Optional; + public enum SignatureAlgorithm { RSASSA_PSS("rsassa-pss"); @@ -39,4 +42,11 @@ public enum SignatureAlgorithm { public String getAlgorithmName() { return algorithmName; } + + public static Optional fromString(String signatureAlgorithm) { + return Arrays + .stream(SignatureAlgorithm.values()) + .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) + .findFirst(); + } } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/SignatureResponseMapper.java index 57391d62..8df028e1 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseMapper.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseMapper.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -40,11 +40,11 @@ import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.dao.SessionCertificate; import ee.sk.smartid.rest.dao.SessionResult; import ee.sk.smartid.rest.dao.SessionSignature; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; public class SignatureResponseMapper { @@ -78,7 +78,7 @@ public static SignatureResponse from(SessionStatus sessionStatus, signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); signatureResponse.setCertificateLevel(certificate.getCertificateLevel()); signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionFlowUsed()); + signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); return signatureResponse; @@ -121,7 +121,7 @@ private static void validateSessionResult(SessionStatus sessionStatus, String re throw new UnprocessableSmartIdResponseException("Document number is missing in the session result"); } - if (StringUtil.isEmpty(sessionStatus.getInteractionFlowUsed())) { + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { logger.error("InteractionFlowUsed is missing in the session status"); throw new UnprocessableSmartIdResponseException("InteractionFlowUsed is missing in the session status"); } @@ -130,14 +130,10 @@ private static void validateSessionResult(SessionStatus sessionStatus, String re throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); } - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); - } - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); validateSignature(sessionStatus); } else { - ErrorResultHandler.handle(endResult); + ErrorResultHandler.handle(sessionResult); } } diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java new file mode 100644 index 00000000..41901325 --- /dev/null +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -0,0 +1,60 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Optional; + +/** + * TrailerField represents the value used in the trailer field of the Smart-ID authentication response and value necessary for generating the signature. + */ +public enum TrailerField { + + OXBC("0xbc", 1); + + private final String value; + private final int pssSpecValue; + + TrailerField(String value, int pssSpecValue) { + this.value = value; + this.pssSpecValue = pssSpecValue; + } + + public String getValue() { + return value; + } + + public int getPssSpecValue(){ + return pssSpecValue; + } + + public static Optional fromString(String trailerField) { + return Arrays.stream(TrailerField.values()) + .filter(field -> field.getValue().equals(trailerField)) + .findFirst(); + } +} diff --git a/src/main/java/ee/sk/smartid/TrustedCACertStore.java b/src/main/java/ee/sk/smartid/TrustedCACertStore.java new file mode 100644 index 00000000..84d10de1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/TrustedCACertStore.java @@ -0,0 +1,56 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +public interface TrustedCACertStore { + + /** + * Get a list of all trusted CA certificates. + * + * @return copy of trusted CA certificates + */ + List getTrustedCACertificates(); + + /** + * Get a set of all trust anchors. + * + * @return copy of trust anchors + */ + Set getTrustAnchors(); + + /** + * Check if OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @return true if OCSP validation is enabled, false otherwise + */ + boolean isOcspEnabled(); +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java new file mode 100644 index 00000000..f5320d26 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java @@ -0,0 +1,36 @@ +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +public class ExpectedLinkedSessionException extends EnduringSmartIdException { + + public ExpectedLinkedSessionException() { + super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java new file mode 100644 index 00000000..f7a117ee --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java @@ -0,0 +1,36 @@ +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +public class ProtocolFailureException extends EnduringSmartIdException { + + public ProtocolFailureException() { + super("A logical error occurred in the signing protocol."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java new file mode 100644 index 00000000..9e3c7a71 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java @@ -0,0 +1,36 @@ +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +public class SmartIdServerException extends EnduringSmartIdException { + + public SmartIdServerException() { + super("Process was terminated due to server-side technical error"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java index b1a55633..436ca2ed 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,7 +31,7 @@ public class RequiredInteractionNotSupportedByAppException extends UserAccountException { public RequiredInteractionNotSupportedByAppException() { - super("User app version does not support any of the allowedInteractionsOrder interactions."); + super("User app version does not support any of the provided interactions."); } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index bf1763e9..414c1003 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,33 +28,7 @@ import java.io.Serializable; -public class AcspV2SignatureProtocolParameters implements Serializable { - - private String rpChallenge; - private String signatureAlgorithm; - private SignatureAlgorithmParameters signatureAlgorithmParameters; - - public String getRpChallenge() { - return rpChallenge; - } - - public void setRpChallenge(String rpChallenge) { - this.rpChallenge = rpChallenge; - } - - public String getSignatureAlgorithm() { - return signatureAlgorithm; - } - - public void setSignatureAlgorithm(String signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { - return signatureAlgorithmParameters; - } - - public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { - this.signatureAlgorithmParameters = signatureAlgorithmParameters; - } +public record AcspV2SignatureProtocolParameters(String rpChallenge, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java index 1b4be422..12bcb09a 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -32,95 +32,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import ee.sk.smartid.SignatureProtocol; -public class AuthenticationSessionRequest implements Serializable { - - private String relyingPartyUUID; - - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - private final String signatureProtocol = SignatureProtocol.ACSP_V2.name(); - - private AcspV2SignatureProtocolParameters acspV2SignatureProtocolParameters; - - private String interactions; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private String initialCallbackURL; - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getSignatureProtocol() { - return signatureProtocol; - } - - public AcspV2SignatureProtocolParameters getSignatureProtocolParameters() { - return acspV2SignatureProtocolParameters; - } - - public void setSignatureProtocolParameters(AcspV2SignatureProtocolParameters acspV2SignatureProtocolParameters) { - this.acspV2SignatureProtocolParameters = acspV2SignatureProtocolParameters; - } - - public String getInteractions() { - return interactions; - - } - public void setInteractions(String interactions) { - this.interactions = interactions; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public String getInitialCallbackURL() { - return initialCallbackURL; - } - - public void setInitialCallbackURL(String initialCallbackURL) { - this.initialCallbackURL = initialCallbackURL; - } +public record AuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + SignatureProtocol signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackURL) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index 0e2d9d9b..979404d4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,25 +28,7 @@ import java.io.Serializable; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; -public class RequestProperties implements Serializable { - - @JsonInclude(JsonInclude.Include.NON_NULL) - Boolean shareMdClientIpAddress; - - public Boolean getShareMdClientIpAddress() { - return shareMdClientIpAddress; - } - - public void setShareMdClientIpAddress(Boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - } - - @JsonIgnore - public boolean hasProperties() { - return shareMdClientIpAddress != null; - } - +public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java new file mode 100644 index 00000000..72c0cb36 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithm implements Serializable { + + private String algorithm; + private SessionMaskGenAlgorithmParameters parameters; + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public SessionMaskGenAlgorithmParameters getParameters() { + return parameters; + } + + public void setParameters(SessionMaskGenAlgorithmParameters parameters) { + this.parameters = parameters; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java new file mode 100644 index 00000000..72eaac4b --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java @@ -0,0 +1,46 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java index 0c107eea..3d638c4f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,22 +33,31 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionResult implements Serializable { - private String endResult; - private String documentNumber; + private String endResult; + private String documentNumber; + private SessionResultDetails details; - public String getEndResult() { - return endResult; - } + public String getEndResult() { + return endResult; + } - public void setEndResult(String endResult) { - this.endResult = endResult; - } + public void setEndResult(String endResult) { + this.endResult = endResult; + } - public String getDocumentNumber() { - return documentNumber; - } + public String getDocumentNumber() { + return documentNumber; + } - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public SessionResultDetails getDetails() { + return details; + } + + public void setDetails(SessionResultDetails details) { + this.details = details; + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java new file mode 100644 index 00000000..a2fc032b --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java @@ -0,0 +1,45 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResultDetails implements Serializable { + + private String interaction; + + public String getInteraction() { + return interaction; + } + + public void setInteraction(String interaction) { + this.interaction = interaction; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java index 45a5aade..4d98d9d8 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java @@ -35,7 +35,10 @@ public class SessionSignature implements Serializable { private String value; private String serverRandom; + private String userChallenge; + private String flowType; private String signatureAlgorithm; + private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; public String getValue() { return value; @@ -53,6 +56,22 @@ public void setServerRandom(String serverRandom) { this.serverRandom = serverRandom; } + public String getUserChallenge() { + return userChallenge; + } + + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + public String getFlowType() { + return flowType; + } + + public void setFlowType(String flowType) { + this.flowType = flowType; + } + public String getSignatureAlgorithm() { return signatureAlgorithm; } @@ -61,4 +80,12 @@ public void setSignatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } + + public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java new file mode 100644 index 00000000..2251f428 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java @@ -0,0 +1,72 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignatureAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + private SessionMaskGenAlgorithm maskGenAlgorithm; + private Integer saltLength; + private String trailerField; + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + public SessionMaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + public Integer getSaltLength() { + return saltLength; + } + + public void setSaltLength(Integer saltLength) { + this.saltLength = saltLength; + } + + public String getTrailerField() { + return trailerField; + } + + public void setTrailerField(String trailerField) { + this.trailerField = trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 3349bbc6..7fdfb52b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,6 +27,7 @@ */ import java.io.Serializable; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) @@ -38,7 +39,7 @@ public class SessionStatus implements Serializable { private SessionSignature signature; private SessionCertificate cert; private String[] ignoredProperties; - private String interactionFlowUsed; + private String interactionTypeUsed; private String deviceIpAddress; public String getState() { @@ -89,12 +90,12 @@ public void setIgnoredProperties(String[] ignoredProperties) { this.ignoredProperties = ignoredProperties; } - public String getInteractionFlowUsed() { - return interactionFlowUsed; + public String getInteractionTypeUsed() { + return interactionTypeUsed; } - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; } public String getDeviceIpAddress() { diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index e1a06daa..219ad562 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -28,15 +28,5 @@ import java.io.Serializable; -public class SignatureAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - - public String getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } +public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java deleted file mode 100644 index c1b8ae79..00000000 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperTest.java +++ /dev/null @@ -1,343 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; - -class AuthenticationResponseMapperTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - - @Test - void from() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionFlowUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - } - - @Test - void from_sessionStatusNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> AuthenticationResponseMapper.from(null)); - assertEquals("Session status parameter is not provided", exception.getMessage()); - } - - @Test - void from_sessionResultIsNotPresent_throwException() { - var sessionStatus = new SessionStatus(); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Session result parameter is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_endResultIsNotPresent_throwException(String endResult) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("End result parameter is missing in the session result", exception.getMessage()); - } - - @Test - void from_endResultIsTimeout_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("TIMEOUT"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(SessionTimeoutException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @NullAndEmptySource - void from_documentNumberIsEmpty_throwException(String documentNumber) { - var sessionResult = toSessionResult(documentNumber); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Document number parameter is missing in the session result", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(signatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Signature protocol parameter is missing in session status", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) - void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(invalidSignatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid signature protocol in sessions status", exception.getMessage()); - } - - @Test - void from_signatureIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Signature parameter is missing in session status", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureValueIsNotProvided_throwException(String signatureValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Value parameter is missing in signature", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_serverRandomIsNotProvided_throwException(String serverRandom) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom(serverRandom); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Server random parameter is missing in signature", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithm); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Signature algorithm parameter is missing in signature", exception.getMessage()); - } - - @Test - void from_sessionCertificateIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Certificate parameter is missing in session status", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Value parameter is missing in certificate", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Certificate level parameter is missing in certificate", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_interactionFlowUsedNotProvided_throwException(String interactionFlowUsed) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionFlowUsed(interactionFlowUsed); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertEquals("Interaction flow used parameter is missing in the session status", exception.getMessage()); - } - - @Test - void from_certificateIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("sha512WithRSAEncryption"); - var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); - - var exception = assertThrows(SmartIdClientException.class, () -> AuthenticationResponseMapper.from(sessionStatus)); - assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); - } - - private static SessionResult toSessionResult(String documentNumber) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber(documentNumber); - return sessionResult; - } - - private static SessionSignature toSessionSignature(String sha512WithRSAEncryption) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("serverRandom"); - sessionSignature.setSignatureAlgorithm(sha512WithRSAEncryption); - return sessionSignature; - } - - private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(AUTH_CERT); - sessionCertificate.setCertificateLevel(QUALIFIED); - return sessionCertificate; - } - - private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); - sessionStatus.setDeviceIpAddress("0.0.0.0"); - return sessionStatus; - } - - private static X509Certificate toX509Certificate(String certificateValue) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateValue.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - private static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); - } -} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index fb4e22be..367569c4 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -36,15 +36,30 @@ import java.security.cert.X509Certificate; import java.time.LocalDate; import java.util.Base64; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; class AuthenticationResponseValidatorTest { @@ -52,34 +67,23 @@ class AuthenticationResponseValidatorTest { private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); private static final String UNTRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); private AuthenticationResponseValidator authenticationResponseValidator; @BeforeEach void setUp() { - authenticationResponseValidator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + authenticationResponseValidator = new AuthenticationResponseValidator(trustedCaCertStore); } - @Disabled("Do not have necessary test data to make this work.") + @Disabled("Can make this work when TEST numbers will be available in the DEMO env") @Test - void toAuthenticationIdentity() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - deviceLinkAuthenticationResponse.setEndResult("OK"); - - deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); - deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - - // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values - deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); - - AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest"); + void validate() { + String rpChallenge = ""; + SessionStatus sessionStatus = new SessionStatus(); + AuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -88,58 +92,16 @@ void toAuthenticationIdentity() { assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); } - @Disabled("Do not have necessary test data to make this work.") + @Disabled("Can make this work when TEST numbers will be available in the DEMO env") @Test - void toAuthenticationIdentity_certificateLevelHigherThanRequested_ok() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - deviceLinkAuthenticationResponse.setEndResult("OK"); - - deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); - deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - - // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values - deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); - - AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, - AuthenticationCertificateLevel.ADVANCED, - "rpChallengeFromTestUserAuthRequest"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } + void validate_certificateLevelHigherThanRequested_ok() { + SessionStatus sessionStatus = new SessionStatus(); + SessionCertificate cert = new SessionCertificate(); + cert.setCertificateLevel("QUALIFIED"); + sessionStatus.setCert(cert); - @Disabled("Do not have necessary test data to make this work.") - @Test - void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidateCertificateLevel_ok() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - deviceLinkAuthenticationResponse.setEndResult("OK"); - - deviceLinkAuthenticationResponse.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - deviceLinkAuthenticationResponse.setInteractionFlowUsed("displayTextAndPIN"); - deviceLinkAuthenticationResponse.setHashType(HashType.SHA512); - deviceLinkAuthenticationResponse.setDeviceIpAddress("0.0.0.0"); - - // TODO - 04.12.24: if device-link authentication can be completed with test number then replace these values - deviceLinkAuthenticationResponse.setSignatureValueInBase64("signatureValueFromTestUserAuthResponse"); - deviceLinkAuthenticationResponse.setServerRandom("serverRandomFromTestUserAuthResponse"); - - AuthenticationIdentity authenticationIdentity = - authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, - null, - "rpChallengeFromTestUserAuthRequest"); + var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -148,158 +110,140 @@ void toAuthenticationIdentity_requestedCertificateLevelIsSetToNull_doNotValidate assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); } - @Test - void toAuthenticationIdentity_certificateHasMatchingIssuerDnAndInvalidSignature_throwsException() { - var validator = new AuthenticationResponseValidator(new X509Certificate[]{toX509Certificate(CA_CERT)}); - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); + @Nested + class ValidateInputs { - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - - deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureData")); - deviceLinkAuthenticationResponse.setServerRandom("serverRandom"); - deviceLinkAuthenticationResponse.setEndResult("OK"); + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("`sessionStatus` is not provided", ex.getMessage()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> validator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallenge")); + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + assertEquals("`authenticationSessionRequest` is not provided", ex.getMessage()); + } - assertEquals("Signature verification failed", ex.getMessage()); + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + assertEquals("`schemaName` is not provided", ex.getMessage()); + } } @Test - void toAuthenticationIdentity_certificateHasMatchingKeyButDifferentDN_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("validSignatureForFakeCert")); - deviceLinkAuthenticationResponse.setServerRandom("serverRandom"); - deviceLinkAuthenticationResponse.setEndResult("OK"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> - authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallenge")); - - assertEquals("Signer's certificate is not trusted", exception.getMessage()); + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Session status field `result` is empty", ex.getMessage()); } - @Test - void toAuthenticationIdentity_deviceLinkAuthenticationResponseIsMissing_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(null, null)); + @Nested + class ValidateSessionStatusCertificate { - assertEquals("Device link authentication response is not provided", exception.getMessage()); - } + @Test + void validate_certificateExpired_throwException() { + var sessionStatus = toSessionsStatus(EXPIRED_CERT, "QUALIFIED", ""); - @Test - void toAuthenticationIdentity_rpChallengeIsNotProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, AuthenticationCertificateLevel.QUALIFIED, null)); + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("RP challenge is not provided", exception.getMessage()); - } + assertEquals("Authentication certificate is invalid", ex.getMessage()); + } - @Test - void toAuthenticationIdentity_certificateValueIsNotProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + @Test + void validate_certificateIsNotTrusted_throwException() { + var sessionStatus = toSessionsStatus(UNTRUSTED_CERT, "QUALIFIED", ""); - assertEquals("Certificate is not provided", exception.getMessage()); - } + var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - @Test - void toAuthenticationIdentity_expiredCertificateProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(EXPIRED_CERT)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + assertEquals("Authentication certificate chain validation failed", ex.getMessage()); + } - assertEquals("Signer's certificate is not valid", exception.getMessage()); - } + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", ""); - @Test - void toAuthenticationIdentity_certificateIsNotTrusted_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(UNTRUSTED_CERT)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - assertEquals("Signer's certificate is not trusted", exception.getMessage()); - } + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } - @Test - void toAuthenticationIdentity_certificateLevelIsNotProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(null); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + @Test + void validate_certificateCannotBeForAuthentication_throwException() { + var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", ""); - assertEquals("Certificate level is not provided", exception.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - @Test - void toAuthenticationIdentity_certificateLevelIsLowerThanRequested_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.ADVANCED); - var exception = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } - assertEquals("Signer's certificate is below requested certificate level", exception.getMessage()); } - @Test - void toAuthenticationIdentity_algorithmNameIsNotProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); + @Nested + class ValidateAuthenticationSignature { - assertEquals("Algorithm name is not provided", exception.getMessage()); - } + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - @Test - void toAuthenticationIdentity_signatureValueIsNotProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); - - assertEquals("Signature value is not provided", exception.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - @Test - void toAuthenticationIdentity_invalidAlgorithmNameIsProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("invalidAlgorithmName"); - deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); - - assertEquals("Invalid signature algorithm was provided", exception.getMessage()); + assertEquals("Authentication signature validation failed", ex.getMessage()); + } } - @Test - void toAuthenticationIdentity_invalidSignatureValueIsProvided_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("invalidSignatureValue")); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); - - assertEquals("Signature verification failed", exception.getMessage()); + private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.OXBC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom(toBase64("a".repeat(43))); + signature.setUserChallenge("TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4"); + signature.setValue(toBase64("signatureValue")); + signature.setFlowType(FlowType.QR.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; } - @Disabled("Do not have necessary test data to make this work.") - @Test - void toAuthenticationIdentity_signatureDoesNotMatch_throwException() { - var deviceLinkAuthenticationResponse = new AuthenticationResponse(); - deviceLinkAuthenticationResponse.setCertificate(toX509Certificate(AUTH_CERT)); - deviceLinkAuthenticationResponse.setCertificateLevel(AuthenticationCertificateLevel.QUALIFIED); - deviceLinkAuthenticationResponse.setAlgorithmName("sha256WithRSA"); - deviceLinkAuthenticationResponse.setSignatureValueInBase64(toBase64("bXhY5CO3gxQ2hxnuQm0Lm/4fXoFPogy4LwS6d0aUu9sZjCfNV5n6IUse45UYLhvmfK4NW5QarlYRTEqIYxlVQ0UMFm6WXQA5AHeOu/JoxKQDnbSeH8Y9FADOnqYXbPWz0W4aFVo0JFoMPO2JrwjC3rFrfded0EkD76vrazzwZxWNkWskC3jJq2Dgu3tsuDdv+Q4moNJYamADQtxYc7a16GNUEklo/ZlUS1pFanDplWTIwGaJd+ZWCvqPrz7cr+PObYfv4NsSN1QBij+eYDS+o6pTK/Ba/ve9AmdR4zS7dv/i1paSmGx3kbm/N0fNn+gelgPv8poOat1TGadT5FLEXWdytDW6I7S+d80xiInPHwKeXI4G4DL+F6zdRw8zWvR6ziXHIkxh/LnioRnoKxOiQZbQbrws2exjyFAS2HkX5UHugPfOkK0YSrJHVpwkOarDAvj7RoOHTFxLd/6FKbugDTG+0tIY4W6RROENePjZW+1eJIOkivO7/iHv3Qi6iIPhW9fB7XUDEtOdmmSrnheU6S9lvKnFYoW3Wcjy12bpK9QoaIzUykzQpO6maOxGr7nQv20AdM6y0vI16Y/8GIEqrGf9V/XVvv5SZFX3BPT3sAsBj0C18imfyyqhU33y1Gr/xMAc0Qbf4Cs92SLczY5yzd1BKGeM3ajaSaHRZbtjRdfiP7xyedyVyWF8COOHVfZb4cXwdpIbtXFkWNcYrfSnhLsRenhIrbKmiDsPRRZCZW8tpDWhr7ge2KY8wb1SbOa38WiNXTjNJAuviZ4ZmUOl5y4CrESdPXN7x7qH+jmfzxUSvBFYaSY2ey46ShHr9zQj7kz3NajIztGK7//sMnQsXuToUnSc5H0XwEwVUT6kSS6ZVYe58quDOgD47Dtj8wczXx081LSXAJXJ75XfxcwJhNn78oHVOR6EqTjOmRLlqj12Bw0WjhzIaut4wQdx0eTXGLqwn6b3RrVoVuwhJ2kwkURe0WDoKa7AWqYZBCHjGlgB3fNEBCNdKLw5ji+0C0jO")); - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.toAuthenticationIdentity(deviceLinkAuthenticationResponse, "rpChallengeFromTestUserAuthRequest")); - - assertEquals("Failed to verify validity of signature returned by Smart-ID", exception.getMessage()); + private static AuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new AuthenticationSessionRequest( + "00000000-0000-0000-0000-000000000001", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2, + new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + null, + null, + null); } private X509Certificate toX509Certificate(String certificate) { @@ -314,4 +258,14 @@ private X509Certificate toX509Certificate(String certificate) { private static String toBase64(String data) { return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); } -} \ No newline at end of file + + private static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } + + private static String toUrlSafeBase64(String data) { + return Base64.getUrlEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java index f2c64180..3d355a7f 100644 --- a/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java @@ -46,6 +46,7 @@ import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.SessionCertificate; import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; import ee.sk.smartid.rest.dao.SessionStatus; public class CertificateChoiceResponseMapperTest { @@ -225,7 +226,24 @@ void from_sessionEndResultIsNotOk_throwException(String endResult, Class CertificateChoiceResponseMapper.from(sessionStatus)); + assertThrows(expectedException, () -> CertificateChoiceResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> CertificateChoiceResponseMapper.from(sessionStatus)); } @Test diff --git a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java new file mode 100644 index 00000000..91833a73 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java @@ -0,0 +1,892 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class DefaultAuthenticationResponseMapperTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + private AuthenticationResponseMapper authenticationResponseMapper; + + @BeforeEach + void setUp() { + authenticationResponseMapper = DefaultAuthenticationResponseMapper.getInstance(); + } + + @Test + void from() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @Test + void from_sessionStatusNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); + assertEquals("Input parameter `sessionsStatus` is not provided", exception.getMessage()); + } + + @Nested + class ValidateResult { + + @Test + void from_sessionResultIsNotPresent_throwException() { + var sessionStatus = new SessionStatus(); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `result` is empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsNotPresent_throwException(String endResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `result.endResult` is empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_endResultIsError_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @NullAndEmptySource + void from_documentNumberIsEmpty_throwException(String documentNumber) { + var sessionResult = toSessionResult(documentNumber); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `result.documentNumber` is empty", exception.getMessage()); + } + } + + + @ParameterizedTest + @NullAndEmptySource + void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(signatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signatureProtocol` is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) + void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(invalidSignatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signatureProtocol` in sessions status", exception.getMessage()); + } + + @Nested + class ValidateSignature { + + @Test + void from_signatureIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature` is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureValueIsNotProvided_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.value` is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|invalidSignatureValue|", "#1234567890"}) + void from_signatureValueDoesNotMatchThePattern_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.value` is not in Base64-encoded format", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_serverRandomIsNotProvided_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.severRandom` is empty", exception.getMessage()); + } + + @Test + void from_serverRandomLengthIsLessThanAllowed_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(23)); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.serverRandom` is less than required length", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|YXRsZWFzdDI0Y2hhcmFjdGVycw|", "#YXRsZWFzdDI0Y2hhcmFjdGVycw"}) + void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.serverRandom` is not in Base64-encoded format", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_userChallengeIsEmpty_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.userChallenge` is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\#dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsd", "dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdW="}) + void from_providedUserChallengeDoesNotMatchThePattern_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("`signature.userChallenge` value in session status is not in the expected Base64-encoded format", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_flowTypeNotProvided_throwException(String flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType(flowType); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.flowType` is empty", exception.getMessage()); + } + + @Test + void from_flowTypeNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.flowType` in session status", exception.getMessage()); + } + + + @ParameterizedTest + @NullAndEmptySource + void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithm); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithm` is empty", exception.getMessage()); + } + + @Test + void from_signatureAlgorithmIsNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.signatureAlgorithm` in the session status", exception.getMessage()); + } + + @Nested + class ValidateSignatureAlgorithmParameters { + + @Test + void from_signatureAlgorithmParametersAreMissing_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters` is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters.hashAlgorithm` is empty", exception.getMessage()); + } + + @Test + void from_hashAlgorithmIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-1"); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status", exception.getMessage()); + } + + @Test + void from_masGenAlgorithmIsMissing_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm` is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(algorithm); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm` is empty", exception.getMessage()); + } + + @Test + void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("invalid"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmInMaskGenAlgorithmIsEmpty_throwException(String hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in empty", exception.getMessage()); + } + + @Test + void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`", exception.getMessage()); + } + + @Test + void from_saltLengthIsMissing_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + signatureAlgorithmParameters.setSaltLength(null); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.saltLength` is empty", exception.getMessage()); + } + + @Test + void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + signatureAlgorithmParameters.setSaltLength(20); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status", exception.getMessage()); + } + + @Test + void from_trailerFieldIsEmpty_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + signatureAlgorithmParameters.setSaltLength(64); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty", exception.getMessage()); + } + + @Test + void from_trailerFieldValueIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("invalid"); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status", exception.getMessage()); + } + } + } + + @Nested + class ValidateCertificate { + + @Test + void from_sessionCertificateIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Certificate parameter is missing in session status", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Value parameter is missing in certificate", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Certificate level parameter is missing in certificate", exception.getMessage()); + } + + @Test + void from_certificateIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); + } + } + + @ParameterizedTest + @NullAndEmptySource + void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUsed) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed(interactionFlowUsed); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Session status field `interactionTypeUsed` is empty", exception.getMessage()); + } + + private static SessionResult toSessionResult(String documentNumber) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber(documentNumber); + return sessionResult; + } + + private static SessionSignature toSessionSignature(String signatureAlgorithm) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("U2VydmVyUmFuZG9tTW9yZVRoYW4yNENoYXJhY3RlcnM="); + sessionSignature.setUserChallenge("dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdWU"); + + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setFlowType("QR"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("0xbc"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + + private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(AUTH_CERT); + sessionCertificate.setCertificateLevel(QUALIFIED); + return sessionCertificate; + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + sessionStatus.setDeviceIpAddress("0.0.0.0"); + return sessionStatus; + } + + private static X509Certificate toX509Certificate(String certificateValue) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateValue.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + private static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } +} diff --git a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java new file mode 100644 index 00000000..7d767574 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java @@ -0,0 +1,81 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class DefaultTrustedCAStoreBuilderTest { + + private static final String TRUST_ANCHOR_CERT = FileUtil.readFileToString("test-certs/TEST_SK_ROOT_G1_2021E.pem.crt"); + private static final String INTERMEDIATE_CA_CERT = FileUtil.readFileToString("trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt"); + private static final String OCSP_CERT = FileUtil.readFileToString("test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer"); + + @Test + void buildDefaultTrustedCACertStore_ocspValidationDisabled() { + X509Certificate trustAnchorCertificate = toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(false) + .build(); + } + + @Disabled("Fails with OCSP response validation error, needs investigation") + @Test + void buildDefaultTrustedCACertStore_ocspValidationEnabled() { + X509Certificate trustAnchorCertificate = toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(true) + .withOCSPValidationCert(toX509Certificate(OCSP_CERT)) + .build(); + } + + private X509Certificate toX509Certificate(String certificate) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 65e35609..19cbf644 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -99,17 +99,17 @@ void initAuthenticationSession_ok() throws Exception { verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); - assertEquals("DEMO", request.getRelyingPartyName()); - assertEquals("QUALIFIED", request.getCertificateLevel()); - assertEquals(SignatureProtocol.ACSP_V2.name(), request.getSignatureProtocol()); - assertNotNull(request.getSignatureProtocolParameters()); - assertNotNull(request.getSignatureProtocolParameters().getRpChallenge()); - assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertNotNull(request.getInteractions()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); - - DeviceLinkInteraction[] parsed = parseInteractionsFromBase64(request.getInteractions()); + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals("QUALIFIED", request.certificateLevel()); + assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertNotNull(request.signatureProtocolParameters().rpChallenge()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + + DeviceLinkInteraction[] parsed = parseInteractionsFromBase64(request.interactions()); assertTrue(Stream.of(parsed).anyMatch(i -> i.getType().is("displayTextAndPIN"))); } @@ -132,7 +132,7 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.getCertificateLevel()); + assertEquals(expectedValue, request.certificateLevel()); } @ParameterizedTest @@ -154,8 +154,8 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); } @Test @@ -175,7 +175,7 @@ void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_o verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertNull(request.getRequestProperties()); + assertNull(request.requestProperties()); } @ParameterizedTest @@ -197,9 +197,9 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertNotNull(request.getRequestProperties()); - assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.getSignatureProtocolParameters().getRpChallenge())); + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); } @ParameterizedTest @@ -220,7 +220,7 @@ void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedException) { - assertThrows(expectedException, () -> ErrorResultHandler.handle(endResult)); + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionResult)); } @ParameterizedTest @ValueSource(strings = {"", "UNKNOWN"}) void handle_unknownEndResult(String unknownEndResult) { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(unknownEndResult)); + var sessionResult = new SessionResult(); + sessionResult.setEndResult(unknownEndResult); + + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(sessionResult)); assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); } + + @Test + void handle_endResultIsUserRefusedInteraction_detailsMissing() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsUserRefusedInteraction_interactionIsEmpty(String interaction) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @Test + void handle_endResultIsUserRefusedInteraction_interactionIsInvalidValue() { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction("invalid interaction"); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Unexpected interaction type: invalid interaction", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void handle_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + } } diff --git a/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java new file mode 100644 index 00000000..a7395253 --- /dev/null +++ b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java @@ -0,0 +1,102 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class FileDefaultTrustedCAStoreBuilderTest { + + @Test + void validateTrustedCaCertificatesOnInitiation_ocspValidationsDisabled() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + assertFalse(trustedCACertStore.getTrustedCACertificates().isEmpty()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPathIsSetToEmpty_throwException(String path) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePath(path) + .build(); + }); + assertEquals("Trust anchor truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePassword(password) + .build(); + }); + assertEquals("Trust anchor truststore password must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePathIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePath(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePassword(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore password must be set", ex.getMessage()); + } + + @Disabled("Not yet implemented") + @Test + void validateTrustedCaCertificatesOnInitiation_withOCSPValidationTurnedOn() { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(true).build(); + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index ef58e2ce..5c7b3964 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -92,12 +92,12 @@ void initAuthenticationSession_ok() { verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals("00000000-0000-0000-0000-000000000000", request.getRelyingPartyUUID()); - assertEquals("DEMO", request.getRelyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2.name(), request.getSignatureProtocol()); - assertNotNull(request.getSignatureProtocolParameters()); - assertEquals("rsassa-pss", request.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertNotNull(request.getInteractions()); + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); } @ParameterizedTest @@ -118,7 +118,7 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.getCertificateLevel()); + assertEquals(expectedValue, request.certificateLevel()); } @ParameterizedTest @@ -139,7 +139,7 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(signatureAlgorithm.getAlgorithmName(), request.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); } @ParameterizedTest @@ -160,8 +160,8 @@ void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertNotNull(request.getRequestProperties()); - assertEquals(ipRequested, request.getRequestProperties().getShareMdClientIpAddress()); + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); } @ParameterizedTest @@ -182,7 +182,7 @@ void initAuthenticationSession_capabilities_ok(String[] capabilities, Set requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), eq(semanticsIdentifier)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); } @Test @@ -118,7 +118,7 @@ void initSignatureSession_withDocumentNumber() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), eq(documentNumber)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); } @ParameterizedTest @@ -137,7 +137,7 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.getCertificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); } @ParameterizedTest @@ -156,7 +156,7 @@ void initSignatureSession_withNonce_ok(String nonce) { SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.getNonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, request.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); } @Test @@ -192,10 +192,11 @@ void initSignatureSession_withRequestProperties() { SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertNotNull(capturedRequest.getRequestProperties()); - assertTrue(capturedRequest.getRequestProperties().getShareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertTrue(capturedRequest.getRequestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } + @Disabled("Signature algorithm has changed") @ParameterizedTest @EnumSource(HashType.class) void initSignatureSession_withSignableHash(HashType hashType) { @@ -215,7 +216,7 @@ void initSignatureSession_withSignableHash(HashType hashType) { assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @ParameterizedTest @@ -234,7 +235,7 @@ void initSignatureSession_withCapabilities(Set capabilities, Set SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @ParameterizedTest @@ -255,7 +256,7 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE, capturedRequest.getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); } @Test diff --git a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java index 531b5e21..eb28a30c 100644 --- a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -32,16 +32,15 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; -import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { @@ -55,11 +54,10 @@ public Stream provideArguments(ExtensionContext context) { Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("USER_REFUSED_DISPLAYTEXTANDPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("USER_REFUSED_VC_CHOICE", UserRefusedVerificationChoiceException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE", UserRefusedConfirmationMessageException.class), - Arguments.of("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", UserRefusedConfirmationMessageWithVerificationChoiceException.class), - Arguments.of("UNKNOWN_RESULT", SmartIdClientException.class) + Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), + Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), + Arguments.of("SERVER_ERROR", SmartIdServerException.class), + Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class) ); } } diff --git a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java index 8963eaae..097eac0e 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java @@ -41,6 +41,7 @@ import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.SessionCertificate; import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; import ee.sk.smartid.rest.dao.SessionSignature; import ee.sk.smartid.rest.dao.SessionStatus; @@ -99,7 +100,7 @@ void from_missingDocumentNumber() { @Test void from_missingInteractionFlowUsed() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setInteractionFlowUsed(null); + sessionStatus.setInteractionTypeUsed(null); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); @@ -204,12 +205,33 @@ void from_unknownSignatureProtocol() { @ParameterizedTest @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) void from_handleSessionEndResultErrors(String endResult, Class expectedException) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getResult().setEndResult(endResult); + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); } + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); + } + @Test void from_sessionStatusNull() { @@ -267,7 +289,7 @@ private static SessionStatus createMockSessionStatus(String signatureProtocol, S sessionStatus.setCert(sessionCertificate); sessionStatus.setSignature(sessionSignature); sessionStatus.setSignatureProtocol(signatureProtocol); - sessionStatus.setInteractionFlowUsed("displayTextAndPIN"); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); return sessionStatus; } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index ec432f2b..1e4eec6c 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java new file mode 100644 index 00000000..597dae6a --- /dev/null +++ b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java @@ -0,0 +1,50 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; + +public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), + Arguments.of("verificationCodeChoice", UserRefusedVerificationChoiceException.class), + Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); + } +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 5e912610..9787b7e7 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -49,33 +50,40 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.DeviceLinkType; -import ee.sk.smartid.HashType; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.rest.dao.HashAlgorithm; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.AuthenticationCertificateLevel; -import ee.sk.smartid.AuthenticationResponse; -import ee.sk.smartid.AuthenticationResponseMapper; +import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.AuthenticationResponseValidator; +import ee.sk.smartid.CertificateByDocumentNumberResult; import ee.sk.smartid.CertificateChoiceResponse; import ee.sk.smartid.CertificateChoiceResponseMapper; import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkType; +import ee.sk.smartid.FileTrustedCAStoreBuilder; +import ee.sk.smartid.HashType; +import ee.sk.smartid.QrCodeGenerator; +import ee.sk.smartid.QrCodeUtil; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; import ee.sk.smartid.SignableData; import ee.sk.smartid.SignatureResponse; import ee.sk.smartid.SignatureResponseMapper; import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.TrustedCACertStore; import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; @@ -84,6 +92,7 @@ public class ReadmeIntegrationTest { private static final String ALPHA_NUMERIC_PATTERN = "^[A-z0-9]{4}$"; + private static final Logger log = LoggerFactory.getLogger(ReadmeIntegrationTest.class); private SmartIdClient smartIdClient; @@ -98,9 +107,9 @@ void setUp() { smartIdClient.setTrustStore(keyStore); } - @Disabled("Demo user account for full dynamic-link flow is not yet available") + @Disabled("These test for created for test-accounts in demo, but these are not currently available device-link flows") @Nested - class DynamicLinkExamples { + class DeviceLinkBasedExamples { @Test void anonymousAuthentication_withApp2App() { @@ -109,42 +118,45 @@ void anonymousAuthentication_withApp2App() { // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response - DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + // Setup builder + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRpChallenge(rpChallenge) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInitialCallbackURL("https://example.com/callback") .withInteractions(Collections.singletonList( - // before the user can enter PIN. If user selects wrong verification code then the operation will fail. DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .initAuthenticationSession(); + )); + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - String sessionId = authenticationSessionResponse.getSessionID(); - // SessionID is used to query sessions status later + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.getSessionID(); + // Following values are used for generating device link or QR-code String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); - + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); // Will be used to calculate elapsed time being used in dynamic link and in authCode Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - // Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + // Build the device link URI (without the authCode parameter) // This base URI will be used for QR code or App2App flows URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") + .withDeviceLinkBase(deviceLinkBase.toString()) .withDeviceLinkType(DeviceLinkType.APP_2_APP) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(sessionToken) .withDigest(rpChallenge) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) .withLang("est") + .withInitialCallbackUrl("https://example.com/callback") .buildDeviceLink(sessionSecret); // Use the sessionId from the authentication session response to poll for session status updates @@ -154,10 +166,11 @@ void anonymousAuthentication_withApp2App() { // Check that the session has completed successfully assertEquals("COMPLETE", sessionStatus.getState()); - // Map the final session status to an authentication response object - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); + // Setup AuthenticationResponseValidator + TrustedCACertStore build = new FileTrustedCAStoreBuilder().build(); + AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(build); // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator().toAuthenticationIdentity(authenticationResponse, rpChallenge); + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -179,42 +192,50 @@ void authentication_withSemanticIdentifierAndQrCode() { // Store generated rpChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response - DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withHashAlgorithm(HashAlgorithm.SHA3_512) .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); + )); - String sessionId = authenticationSessionResponse.getSessionID(); - // SessionID is used to query sessions status later + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.getSessionID(); + // Following values are used for generating device link or QR-code String sessionToken = authenticationSessionResponse.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + // Will be used to calculate elapsed time being used in dynamic link and in authCode Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - // Generate QR-code or device link to be displayed to the user using sessionToken and sessionSecret provided in the authenticationResponse + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); // Build the device link URI (without the authCode parameter) // This base URI will be used for QR code or App2App flows URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(sessionToken) .withDigest(rpChallenge) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); // Use sessionId to poll for session status updates SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); @@ -222,14 +243,11 @@ void authentication_withSemanticIdentifierAndQrCode() { // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. assertEquals("COMPLETED", sessionStatus.getState()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - // Map the final session status to an authentication response object - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - - // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator() - .toAuthenticationIdentity(authenticationResponse, rpChallenge); + // Validate the response and return user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(trustedCaCertStore) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -246,17 +264,18 @@ void authentication_withDocumentNumberAndQrCode() { // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication session status OK response - DeviceLinkSessionResponse authenticationSessionResponse = smartIdClient + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withHashAlgorithm(HashAlgorithm.SHA3_512) .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") - )) - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get AuthenticationSessionRequest after the request is made and store for validations + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later @@ -265,12 +284,13 @@ void authentication_withDocumentNumberAndQrCode() { // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.getSessionSecret(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); // Generate the base (unprotected) device link URI, which does not yet include the authCode long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) .withSessionToken(sessionToken) .withDigest(rpChallenge) @@ -278,6 +298,9 @@ void authentication_withDocumentNumberAndQrCode() { .withElapsedSeconds(elapsedSeconds) .withLang("est") .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); // Use sessionId to poll for session status updates SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); @@ -285,14 +308,11 @@ void authentication_withDocumentNumberAndQrCode() { // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - - // Map the final session status to an authentication response object - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator() - .toAuthenticationIdentity(authenticationResponse, rpChallenge); + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(trustedCaCertStore) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -300,6 +320,76 @@ void authentication_withDocumentNumberAndQrCode() { assertEquals("EE", authenticationIdentity.getCountry()); } + @Test + void signature_withDocumentNumberAndQRCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA256); + + // Build the dynamic link signature request + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withHashAlgorithm(HashAlgorithm.SHA_256) + .withInteractions(List.of( + DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.getSessionID(); + String sessionToken = signatureSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.getSessionSecret(); + Instant receivedAt = signatureSessionResponse.getReceivedAt(); + URI deviceLinkBase = signatureSessionResponse.getDeviceLinkBase(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + @Test void signature_withSemanticIdentifier() { var semanticIdentifier = new SemanticsIdentifier( @@ -325,7 +415,8 @@ void signature_withSemanticIdentifier() { SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); - // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); // Create the signable data var signableData = new SignableData("dataToSign".getBytes()); @@ -340,8 +431,6 @@ void signature_withSemanticIdentifier() { // Build the dynamic link signature request DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) @@ -428,14 +517,10 @@ void authentication_withDocumentNumber() { assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - // validate sessions status result and map session status to authentication response - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - // validate certificate value and signature and map it to authentication identity - var authenticationResponseValidator = new AuthenticationResponseValidator(); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + // validate the sessions status and return user's identity + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(new FileTrustedCAStoreBuilder().build()) + .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: authentication request will be fixed with notification-based authentication changes - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, rpChallenge); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); @@ -481,14 +566,9 @@ void authentication_withSemanticIdentifier() { assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - // validate sessions status result and map session status to authentication response - AuthenticationResponse authenticationResponse = AuthenticationResponseMapper.from(sessionStatus); - // validate certificate value and signature and map it to authentication identity - var authenticationResponseValidator = new AuthenticationResponseValidator(); - // if sessions end result is something else than OK then exception will be thrown, otherwise continue to next step + AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(new FileTrustedCAStoreBuilder().build()) + .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: will be fixed with notification-based authentication changes - // validate certificate value and signature and map it to authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.toAuthenticationIdentity(authenticationResponse, rpChallenge); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); @@ -527,7 +607,7 @@ void certificateChoice_withSemanticIdentifier() { } @Test - void signature_withSemanticsIdentifier(){ + void signature_withSemanticsIdentifier() { var semanticIdentifier = new SemanticsIdentifier( // 3 character identity type // (PAS-passport, IDC-national identity card or PNO - (national) personal number) diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index a6345af0..f4ebac93 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -42,10 +42,10 @@ import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashType; import ee.sk.smartid.InteractionUtil; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SignatureAlgorithm; +import ee.sk.smartid.SignatureProtocol; +import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.SmartIdRestConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; @@ -53,12 +53,14 @@ import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @Disabled("Relying party demo account not yet available for v3") @@ -81,9 +83,9 @@ void setUp() { smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); } - @Disabled("Demo account for dynamic-link requests not yet available") + @Disabled("Demo accounts for device link requests not yet available") @Nested - class DynamicLink { + class DeviceLink { @Nested class Authentication { @@ -125,16 +127,20 @@ void initDeviceLinkAuthentication_withSemanticsIdentifier() { } private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - - var signatureParameters = new AcspV2SignatureProtocolParameters(); - signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signatureParameters.setRpChallenge(RpChallengeGenerator.generate()); - request.setSignatureProtocolParameters(signatureParameters); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?")))); - return request; + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + + return new AuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureParameters, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + null, + null, + null); } } @@ -233,23 +239,21 @@ void initNotificationAuthentication_withDocumentNumber() { } private static AuthenticationSessionRequest toAuthenticationRequest() { - var request = new AuthenticationSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setCertificateLevel("QUALIFIED"); - - String randomChallenge = RpChallengeGenerator.generate(); - var signatureParameters = new AcspV2SignatureProtocolParameters(); - signatureParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signatureParameters.setRpChallenge(randomChallenge); - request.setSignatureProtocolParameters(signatureParameters); - - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?")))); - - var requestProperties = new RequestProperties(); - requestProperties.setShareMdClientIpAddress(true); - request.setRequestProperties(requestProperties); - return request; + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + + return new AuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureParameters, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + new RequestProperties(true), + null, + null + ); } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 9c400def..73089982 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -40,11 +40,13 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; import java.time.Instant; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; @@ -55,6 +57,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.InteractionUtil; +import ee.sk.smartid.SignatureProtocol; import ee.sk.smartid.SmartIdRestServiceStubs; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; @@ -77,6 +80,9 @@ import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @@ -120,16 +126,28 @@ void getSessionStatus_running_withIgnoredProperties() { void getSessionStatus_forSuccessfulAuthenticationRequest() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); assertSuccessfulResponse(sessionStatus); - assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); + assertEquals("displayTextAndPIN", sessionStatus.getInteractionTypeUsed()); assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - assertNotNull(sessionStatus.getSignature()); - assertThat(sessionStatus.getSignature().getValue(), startsWith("TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZ")); - assertEquals("sha512WithRSAEncryption", sessionStatus.getSignature().getSignatureAlgorithm()); - assertEquals(SERVER_RANDOM, sessionStatus.getSignature().getServerRandom()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionSignature.getValue())); + assertEquals(SERVER_RANDOM, sessionSignature.getServerRandom()); + assertTrue(Pattern.matches("^[a-zA-Z0-9-_]{43}$", sessionSignature.getUserChallenge())); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + assertEquals("SHA3-512", signatureAlgorithmParameters.getHashAlgorithm()); + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); + SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); + assertEquals("SHA3-512", parameters.getHashAlgorithm()); + assertEquals(64, signatureAlgorithmParameters.getSaltLength()); + assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww")); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); } @@ -147,7 +165,7 @@ void getSessionStatus_forSuccessfulCertificateRequest() { void getSessionStatus_forSuccessfulSignatureRequest() { SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); assertSuccessfulResponse(sessionStatus); - assertEquals("verificationCodeChoice", sessionStatus.getInteractionFlowUsed()); + assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); assertNotNull(sessionStatus.getSignature()); @@ -186,58 +204,96 @@ void getSessionStatus_whenSessionNotFound() { }); } - @Test - void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED"); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE"); + @Nested + class UserRefusedInteractions { + + @Test + void getSessionStatus_userHasRefused() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessage() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessage", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessageAndVerificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedDisplayTextAndPin() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("displayTextAndPIN", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("verificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } } @Test - void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE"); + void getSessionStatus_userHasRefusedCertChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_CERT_CHOICE", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_userHasRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_DISPLAYTEXTANDPIN"); + void getSessionStatus_timeout() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("TIMEOUT", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_userHasRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_VC_CHOICE"); + void getSessionStatus_userHasSelectedWrongVcCode() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("WRONG_VC", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_userHasRefusedCertChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "USER_REFUSED_CERT_CHOICE"); + void getSessionStatus_documentUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("DOCUMENT_UNUSABLE", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "TIMEOUT"); + void getSessionStatus_protocolFailure() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-protocol-failure.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PROTOCOL_FAILURE", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "WRONG_VC"); + void getSessionStatus_expectedLinkedSession() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-expected-linked-session.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("EXPECTED_LINKED_SESSION", sessionStatus.getResult().getEndResult()); } @Test - void getSessionStatus_whenDocumentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); - assertSessionStatusErrorWithEndResult(sessionStatus, "DOCUMENT_UNUSABLE"); + void getSessionStatus_serverError() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-server-error.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); } private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { @@ -252,10 +308,6 @@ private static void assertSuccessfulResponse(SessionStatus sessionStatus) { assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); } - private static void assertSessionStatusErrorWithEndResult(SessionStatus sessionStatus, String endResult) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals(endResult, sessionStatus.getResult().getEndResult()); - } } @Nested @@ -398,6 +450,7 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18082"); } + @Disabled("Request body has changed") @Test void initNotificationAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); @@ -414,6 +467,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { }); } + @Disabled("Request body has changed") @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { @@ -437,6 +491,7 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18083"); } + @Disabled("Request body has changed") @Test void initNotificationAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); @@ -453,6 +508,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { }); } + @Disabled("Request body has changed") @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { @@ -648,13 +704,17 @@ void getCertificateByDocumentNumber_successful() { @Test void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); - assertThrows(UserAccountNotFoundException.class, () -> {connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest());}); + assertThrows(UserAccountNotFoundException.class, () -> { + connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); + }); } @Test void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> {connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest());}); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> { + connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); + }); } } @@ -805,6 +865,7 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18084"); } + @Disabled("Request body has changed") @Test void initNotificationSignature() { SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); @@ -833,6 +894,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { }); } + @Disabled("Request body has changed") @Test void initNotificationSignature_requestIsUnauthorized_throwException() { SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json"); @@ -911,6 +973,7 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18085"); } + @Disabled("Request body has changed") @Test void initNotificationSignature() { SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); @@ -939,6 +1002,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { }); } + @Disabled("Request body has changed") @Test void initNotificationSignature_requestIsUnauthorized_throwException() { SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json"); @@ -1003,40 +1067,41 @@ void initNotificationSignature_throwsServerMaintenanceException() { } private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { - var dynamicLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); - dynamicLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); - dynamicLinkAuthenticationSessionRequest.setCertificateLevel("QUALIFIED"); - - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); - signatureProtocolParameters.setRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())); - signatureProtocolParameters.setSignatureAlgorithm("rsassa-pss"); - dynamicLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); - - var algorithmParameters = new SignatureAlgorithmParameters(); - algorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getValue()); - signatureProtocolParameters.setSignatureAlgorithmParameters(algorithmParameters); - - DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Log in?"); - dynamicLinkAuthenticationSessionRequest.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); - - return dynamicLinkAuthenticationSessionRequest; + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + + return new AuthenticationSessionRequest( + "00000000-0000-0000-0000-000000000000", + "DEMO", + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + null, + null, + null + ); } private static AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { - var deviceLinkAuthenticationSessionRequest = new AuthenticationSessionRequest(); - deviceLinkAuthenticationSessionRequest.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - deviceLinkAuthenticationSessionRequest.setRelyingPartyName("DEMO"); - - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(); - signatureProtocolParameters.setRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())); - signatureProtocolParameters.setSignatureAlgorithm("rsassa-pss"); - deviceLinkAuthenticationSessionRequest.setSignatureProtocolParameters(signatureProtocolParameters); - - NotificationInteraction interaction = NotificationInteraction.verificationCodeChoice("Verify the code"); - deviceLinkAuthenticationSessionRequest.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); - - return deviceLinkAuthenticationSessionRequest; + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters("SHA-512")); + + return new AuthenticationSessionRequest( + "00000000-0000-0000-0000-000000000000", + "DEMO", + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeInteractionsAsBase64(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))), + null, + null, + null + ); } private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { diff --git a/src/test/resources/responses/session-status-expected-linked-session.json b/src/test/resources/responses/session-status-expected-linked-session.json new file mode 100644 index 00000000..ea5b572f --- /dev/null +++ b/src/test/resources/responses/session-status-expected-linked-session.json @@ -0,0 +1,8 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "EXPECTED_LINKED_SESSION", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file diff --git a/src/test/resources/responses/session-status-protocol-failure.json b/src/test/resources/responses/session-status-protocol-failure.json new file mode 100644 index 00000000..77efaa4d --- /dev/null +++ b/src/test/resources/responses/session-status-protocol-failure.json @@ -0,0 +1,8 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "PROTOCOL_FAILURE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file diff --git a/src/test/resources/responses/session-status-server-error.json b/src/test/resources/responses/session-status-server-error.json new file mode 100644 index 00000000..d9e2716a --- /dev/null +++ b/src/test/resources/responses/session-status-server-error.json @@ -0,0 +1,8 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "SERVER_ERROR", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-authentication.json b/src/test/resources/responses/session-status-successful-authentication.json index 5fca38cb..bdda60ba 100644 --- a/src/test/resources/responses/session-status-successful-authentication.json +++ b/src/test/resources/responses/session-status-successful-authentication.json @@ -3,20 +3,36 @@ "result": { "endResult": "OK", "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, "signatureProtocol": "ACSP_V2", "signature": { "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", - "signatureAlgorithm": "sha512WithRSAEncryption", "serverRandom": "J0iyCYOu8cTWuoD8rD05IIrZ", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "userChallenge": "GnsWXXEjTCKR89fj9uo5u5ReBZ9JR7_pezLAI5jMS00", + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA3-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, "cert": { "value": "MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQybEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7CXahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEOTnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5nzCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcyhPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29JcowTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9PsmN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1JgxBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRjFisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QACBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v09hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSgAt+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0GCysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rlc3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHKmTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0XnPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxWCbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2", "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "interactionFlowUsed": "verificationCodeChoice", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "interactionTypeUsed": "displayTextAndPIN", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json index 2c6082e0..e009ee1b 100644 --- a/src/test/resources/responses/session-status-successful-signature.json +++ b/src/test/resources/responses/session-status-successful-signature.json @@ -16,6 +16,6 @@ "certificateLevel": "QUALIFIED", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "interactionFlowUsed": "verificationCodeChoice", + "interactionTypeUsed": "verificationCodeChoice", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } diff --git a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json index 7b29249a..62656ae6 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json @@ -1,8 +1,12 @@ { "state": "COMPLETE", "result": { - "endResult": "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessageAndVerificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-confirmation.json b/src/test/resources/responses/session-status-user-refused-confirmation.json index bf455a60..d3b1af6e 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation.json @@ -1,8 +1,12 @@ { "state": "COMPLETE", "result": { - "endResult": "USER_REFUSED_CONFIRMATIONMESSAGE", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessage", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json index ecdd6577..be9fecf6 100644 --- a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json +++ b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json @@ -1,8 +1,12 @@ { "state": "COMPLETE", "result": { - "endResult": "USER_REFUSED_DISPLAYTEXTANDPIN", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "displayTextAndPIN", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-vc-choice.json b/src/test/resources/responses/session-status-user-refused-vc-choice.json index 8e897207..a06e5f09 100644 --- a/src/test/resources/responses/session-status-user-refused-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-vc-choice.json @@ -1,8 +1,12 @@ { "state": "COMPLETE", "result": { - "endResult": "USER_REFUSED_VC_CHOICE", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "verificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/sid_trust_anchor_certificates.jks b/src/test/resources/sid_trust_anchor_certificates.jks new file mode 100644 index 0000000000000000000000000000000000000000..309153e2753f233601d2ddf24ee06383ce1e312c GIT binary patch literal 4541 zcmc(i2{_d28o+0_4q3ujGPV%uKf@S8Xvmh-%p^;4(3E{0QuZZ#TsbHqyX;B2mXYkT zgd-G5%9edei^3h%aXa^(=eg(J=iYOldH(Z%-|zeW&-Xsx_rAaPeYY027C;~ng#N(3 zzJ)0iv<=0bYIniT#)@j^;!M|^sqPfCyNe4IZDR!jfs-3+dgMTG6FD59O#h4l2!=s{ zV8~ns6at1o_K*WVWq2&8gN}DZK6~Rfr4JwxOb5br!#*IvU^pBEcw$%pCIs{}48aA_ z*2ahe=pBWb6{Sf+lfLat^`KDP?VRj994Hj>YhBkov7 z(8TZoJiEIwfq9r*9qcqb9ONhzK$MRi3t;F!`S1T`I|FnDi@{^%amqMlv;UWA!C=uJ z4+#thL4(2UARrjb3<(BA^RHSAiWs~T*R&Qtv#@ScW36h-t!Kra6=hPeSLR}Q zjgp=*`^0D2IJU(cm%ChF0RLilHu0MEr)t-Qrc-{s2~&=9fWbJ;ubz18iL(!7RjK?@ z7Iw+{p6x3uQiEdR_WaK^ilwQ7`B?Tz-}|jD)J-40nc_1G2($qfFfrfkzB85i#g!I& z-D4y<-&dqmtzlg^+)=Xbwgffk^xY4h@6~e7I}934KhY>*AI0|5qBu!69!GxOBV_2` z?JsPM6mh-!9vve5zD5`pH+v}p*StM@A~V`h-9|vGN$aVESI1wk4eisvaK8I&OKQk? zBV$C${E6(9BZ9Ke9fdLtAW$#}oE;Pe1cd|abVm^40)w|^lQ6y8j^n zp&gwK#slN#Ts1o~Yb;pbjVNAOk$7BS@Ra)|puICK0XqVyA{b>5bcbYsLUtwat_0nc zV7n6IuEg}?bCPUk%S>ZT4ql)K*@=ua8S`JJU#{3**l!n{( z%0Y0&XJz?;wA4@c2dnZAnA41pi^?4%M2Tx0T;r@#apliRs^%{X*W|b7Ln4gYjyP9K z8zS{0Il{5#QwFg1iL+BwkDo1gZg3@TeI6;-nmp`UjCYu-kaw4QTyZAjJ%lF$!SW!_ z04??OrCM|e9!{ceId{jwGyYpxd7e~(>hUwMc-E|RemMV@9|8c1ex-DNaFPtrdRl1G z?v|P)3@@;EXVQw5?F5;qt$h#!;1%!yMjj&#NbXEPS zH@%3u-#AtJPGPB7d+pUqz92k^H{gkzj}%byRx6~Qq0-V`ZyhC*v`0;Si#Cb3vcH9@ zc77n5dq%YiPaB^RWoew*$EOcss?^UC>Oe9rl9~{h&fYQMilAVSob^> zeud2oN2|3@GMg^b@-=QBJA~r5(A8^}$`1n1YY1u0eJZ2$Z6YhU(!5?3hu0T~xOYBz zNQh#I9;UTclg14o^+6Rn8@_i`{5CG%#y+6*Jw5qLH~|+SE0pt>TYFW-)1pew zkLaXL-qT<$+6=VqTyIK9f0QC+$$HT3RV2@C-U3BNq0T!!-rI=EteU$_%-)v7 z*}R&}T;UYSbrEu{^|@)oV=*QR4q!2kghlh7)Y2o@2Np~jn1a6430_!dxCILJ>(IM6 zx1Dd-RIZ|avK^4%TA}rBCWA3xr%7?+w{vOXF*%fcM{zWrg8v`|!!+KNlHL4igti;(=TW?&HdVQVesF|4g zmnxqo5qvCUCQilRkVWJbpg>_hJSdGIWvycIVieQ$I*~VH!`=Nvf`0U}+nF1#@K(W) zpisp)iLCaA**!swwFDsXt{UGbUrp0|W*?$S@2fWgcI~~%Zy)#RowXbrmM;n88I8mB zre&KcoV@;0G9@!v6FevO#My~ev6o3bm=_vq@QTUbu{;vM%_5|Gm_8=I( z9tXYTfy)HJKw{9KFnS3e)>fQ8ay3wybA5S~vv8JiRN?XHkQYa*LB%}EMtjgkez;Fg zYMJwY9Lz=4+u6?3>Y^aOBBT(A3Z`S6^Pr{x{L{TJWXK zi-#XW{lO}727%?{0Rt;jzd_IMF8IM6ph3|fdT9vJR8YL6Q<%3u>ys11yTQ;8bG^-| z#_=+TNBw$zlmqE@SzZ2R@tQ9D))tc2R#D=hnxZVG>t;F=F*izDz1|Oj%QzrM!8()0 zs^=$cOQYsws&N z=+_d`ZCtLLZQ(sSsBgo@B(JP`vctEjjeUh#wK_~NY;5mGt&PF;^qk2A67Ep>lLtQ$ zo>!BKVmcAhZEly4CIX(2(Pb5ul*to4hB=$6QpZM})r+0Z42SjlAZTN_BDWa;2F#xce2lB+8?_4*6^^>2~ z37a~}_}%;%LAGqE4zN#D_v;&i2TK>~59SPQK1k+yq!+hzbvce>Zich{U{m}#!>xo| zlP}M0gmZ*Y96lRDo?063A|E$KjHEYgbtR-cH|v%zK-ODLR|iI9`7Y-Fhuu}B0&`mK zp*=0rB5V(v%PBenP_M23VRt3AL~&}4Ih=pf-urn{CYx$p!}Lk6BeT{DVw2~O*1`N3 zEpM{v@6B^fjVd=QXLsPXv4K;Y^C}^-r*lZR2Gx_RO)~N}woXg3z=a@Ct zb(NoPi%d1dcAe=n=jQdFbg!x$AE0TUx|?Rp%P81l;-2*}Uf-)-ExRmXL9#g`x0GCi zb0(SIz9*3GvHU7j*DHD^Sv2Kn%Qe1V%0Yu@0|IS}^S<8_x`q9SXj;T`#5LkCR4LbZ zr?m|y7>#k2eR!HrI@?>X+Do*hw(>a6tTeCnx!&?x7w3!V#e_l_my&a<-AzR=G4b1m puMhUM@>KZ(^?7Gqh%tor<>Q<{UFgd8eKley;$>8eW3;2_-vF~io7n&W literal 0 HcmV?d00001 diff --git a/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt new file mode 100644 index 00000000..054743a0 --- /dev/null +++ b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAiagAwIBAgIQGjWemJjC5ORg6CkyNQ5DzTAKBggqhkjOPQQDBDBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzA5MTA0NzE0WhcNNDEwNzA5MTA0NzE0WjBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABACGx6ye24WAORL1 +8N0SquoI3TTJ3dd2EcZLs+wZY0XWYzPa0S4o8BKZQTCDbXz9O2x94hpdAjZ4S3Q2 +N7DAvQ0FfAHmM2JotR4UnYvxYv4JxJHpoRvrQoXOXdqO/wMymiPKTXHPFQz6nxxa +ORjy8xsrQeIdrTLj3c+HDVBRA5yE/IXed6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFOIc3mPcvviEfgE7LkuAseF/1fHmMB8G +A1UdIwQYMBaAFOIc3mPcvviEfgE7LkuAseF/1fHmMAoGCCqGSM49BAMEA4GLADCB +hwJBNDZ3R6qmJqL5bQf01oT369DEGcLhr2vA00nRZSqeaaLMfq+RQW8aYl0njfIZ +JAC6q6IJklpH5IyYrcZ29tcBrxECQgFH5aw8ZORororrLDPl1yY2RgsCO1SFoDh5 +eMEaKVtRKNSG1jLzfgiZJOdtIj/h/l/4oDc5DrDDY6kbAnl4M5pDKw== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer new file mode 100644 index 00000000..b9da8a0c --- /dev/null +++ b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 +IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN +MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp +Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg +b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr +LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik +gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd +r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs +z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9 +OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb +wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg +RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE +FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D +AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A +aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w +JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME +GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw +czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN +BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR +aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo +MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA +nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24 +mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0 +dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates.jks b/src/test/resources/trusted_certificates.jks new file mode 100644 index 0000000000000000000000000000000000000000..d7f73f58b605364e712d945ed5a0452fc4330240 GIT binary patch literal 4573 zcmeHLc{r3^8=q%pj9o&OA`_8pb7nA%ELldV>|3LViQC`RE$~5(0q<23$-a;0ihNQAh}hl=jXW z?90MQX?fpr8V(?KP=F*xRGVkd`zQ%P5<&c7vCl5C=Sq~444UGZb051#Xe)U+)$yLvNB~}nRz(uNk9}Gwcf#M?PAmHa> zNG=jWJWG;WJx8IgmwRN$|BYg?3wysasy9r6AI~&#yW#cfAnuh^55Hri$l4iyXzi|r zzZ~LK2o;!;uBVb*@8WM6XfxUu9KY`pC=z!2?dr2wXaT72%A{1A)+STQYB|$jr!24-QjFaY1g$h zyG)+Qy>29wq7=9y&|cEp^wqgsM}HIltTN-H+PodcoxZg8ZU=@?%3|}8&N-237uju^ zhSJN^6SESdL)sr$8aGdf_MSOTJ@m16U!Jbt21MB~G`hv)!rUvbTT-4?@o#4uIQOy5FML-O^Di`OfL-NNN(v^F?Fe>#+Xl zhSMR79$Sv)N49Lfnh_G>+F!7GvpYZkXd2Ye4r((aDJSGkA+%F z=1>Pqr}TKw7srsZdynTwFh@pLl;&fe9^@;9(H|@DL;2aCbS5<#$)}sP(^kFPd@y@w zQgFFOflhfi5(OckY%UT$5zshLg%6O5IvPIFv2rpWC>Fh5%2lXCPdpPy4F{_!}ag6VlUq<#<@vsCUeyKpTcO^V7iK&fUKJ?i@Tjm>&(}wd=FBr)m*j-W0{T0Ix;CTwKAw@2K(v<>uY3a+lLR~Tm!Oe zla*tYr~w@sItkV_2gcmji~9c4`uYG>uJ5Gey-=!zulL2E$OT~Nq<*~_^!T)_QBH`_p|LG=(7&+?M!aGG4i_ZLUgpTXg?iMZ4ygH%G5kf8NsyzY-)uoJJ4&6%vVj zj{k;4-7pe8hmq*sG!iX~H4Ud<)86NO%S8yk;AYEj!umgeL=Y+E*#6s4bp9Vra?jKo7KJJ%9^KaJx(&YsW&5J-(|NVj26D{B{tO$Ub! zk@LEszBBm^^WNR>ij(@7(`c8_d9Zia?Y@oMwf3yJ#M6!O;|t3U5Y<{9y+QiQUp^D} zAX3;rV4huOn~2q=)S=ULhxFd>TU1Wb$ZS-f$g4R_^RoCzEJ{*WeRFtV;%6jp*=qSF z)7RHv8ZQ%>X_P*dKw z;g_Kc%aZgK4w>ebyT}qc--$LaiCn>nHcTVAopoa#Qs!;e)s@&uxzBwhIgX?*yn1zQ z1|`O-G)MdAO$#d&$tuC4&DI`()pcm_*mcA4&h()@D!Y8PIhX9_c6*-D{ZM?(u=;cB z%4CVT?E!>S&gU=p)iR42%0&+w?GN7+H<#fhA#OTNMWe z?J^HI*|)yTK-C_efub0goNW6mP?RQ`E1V!(v&?C7qH^7tg#5U&kGN zBvO9gqy2F+Wzwdqo-h#NAC!CNV7HfsNr)g+W!)NM2NA;?fwGs(5*m9c$j z>Sx>C%AZ+%6PfiFlBrjNYRiLpiFt+kITKZ*50W$G1_P~C*pJZCIwaex9;u6m7bKc3 z$s5N}iPgMxbWOeDk+SOTK2h~gpB0?U(ltJKx+>q-8xi$HtmeWzFZ{Zuh*k-a^tiU; z8jkyqn+fI6E8`TRnb;-bJBKfOeo?U`Bqo_PFdmvTsjF;2x=lQnt>_H40bWl$Pi^HU zRrK#T@a)kYEw{P8CjAab?+*QtrZ*3SWiKJ-OB9QhBo|B}p&f_D=3p7YSavv;9)x8x zHbe>Z5F0B%$c?bxAQ;lHT0a`Z63C?xJZ&i~=Z@Vw>zFYMqNp0cMcsj~rC&i*4tsJF zMT)qJEh6qAV|q02_Qd2K zhk9x06orIQ3hP03_Ymysx7KdX&K}qmcrsY1AW1L;L|}jy$TtE-LDGN(j12HoT|5PB z#OnfWfeR=J^&d?pfI`=)Ad!FU)1U6-dkqUV0cnizR6t;&Eu<5NMyR0z5$KU8Z#%Yr z&P;!|w}jNZMtaahCS9h>G5O6q{mlJYq~NEwoNC0MW%GZr9xxr=a;LN3Gz8=MDYFXa zQ_8g^p1;|aL^gF)EhK+jHsq=I)cLdq)rPXS)mJm;Vn&$@5A{f)g0zMCzUn@QxgaBB4z{MW{)=fPEUch6<9+n{X z0{d?ggdQ||vh`_Euo_`AqF7WK8z&(C@Rx$GUjg)(tN<#9PNnIwIRPA61kC<%fFzh+ z6-@vD=<{{?+B~gk*ZzD{0{Z<;Srsi-P}>S1qvYgVSHfK2YEt?MzC<+ z+|^wbtD<%X7VEbtERLrHrXj5%*OKZjny=R)GpBk!M${&|G{RI(Eh`8l_ccSb@ zf8@R=%2tOLXE_ Date: Wed, 9 Jul 2025 13:35:15 +0300 Subject: [PATCH 28/57] SLIB-106 - added device-link signature sessions status validations (#118) * SLIB-106 - added device-link signature sessions status validations * SLIB-106 - fixed naming, addad TODO * SLIB-106 - refactured, added tests --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 51 +- README.md | 10 +- .../java/ee/sk/smartid/SignatureResponse.java | 67 ++- .../sk/smartid/SignatureResponseMapper.java | 201 ------- .../smartid/SignatureResponseValidator.java | 436 +++++++++++++++ .../smartid/SignatureResponseMapperTest.java | 302 ---------- .../SignatureResponseValidatorTest.java | 519 ++++++++++++++++++ .../integration/ReadmeIntegrationTest.java | 20 +- 8 files changed, 1077 insertions(+), 529 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/SignatureResponseMapper.java create mode 100644 src/main/java/ee/sk/smartid/SignatureResponseValidator.java delete mode 100644 src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c7ab18d..327e793e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,23 +2,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [3.1.3] - 2025-07-05 +## [3.1.6] - 2025-07-08 -### Changed -- Updates to session status response - - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. - - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling - - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` - - Renamed `interactionFlowUsed` to `interactionTypeUsed`. -- Updated AuthenticationSessionRequest and related classes to records. -- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. - - Created to builder-classes for loading trusted CA certificates - - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore - - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates -- Refactored AuthenticationResponseMapper to be used as singleton instead of static class and added it as dependency for AuthenticationResponseValidator -- Update AuthenticationResponseValidator - - update signature value validation - - added additional certificate validations (validate certificate chain and certificate purpose) +### Added +- Session-status (signature) + - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. + - Allowed `flowType`: QR · App2App · Web2App · Notification. + - Fixed `signatureAlgorithm` to `rsassa-pss`. + - `signatureAlgorithmParameters` + - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. + - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. + - `saltLength`: 32 / 48 / 64 bytes to match chosen hash. + - `trailerField`: `0xbc`. + +- Certificate + - Must be a Smart-ID *signature* certificate: + - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. + - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. + - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. ## [3.1.5] - 2025-06-30 @@ -30,6 +31,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `initialCallbackURL` field with regex validation. - Added `deviceLinkBase` to session response. +## [3.1.4] - 2025-07-05 + +### Changed +- Updates to session status response + - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. + - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling + - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` + - Renamed `interactionFlowUsed` to `interactionTypeUsed`. +- Updated AuthenticationSessionRequest and related classes to records. +- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. + - Created to builder-classes for loading trusted CA certificates + - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore + - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates +- Refactored AuthenticationResponseMapper to be used as singleton instead of static class and added it as dependency for AuthenticationResponseValidator +- Update AuthenticationResponseValidator + - update signature value validation + - added additional certificate validations (validate certificate chain and certificate purpose) + ## [3.1.3] - 2025-06-13 ### Added diff --git a/README.md b/README.md index 2ef14ad4..7640d6f8 100644 --- a/README.md +++ b/README.md @@ -809,8 +809,16 @@ try { ```java try { + // Get the session status response + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .build(); + + // Initialize the validator with the CA store + SignatureResponseValidator validator = new SignatureResponseValidator(trustedCACertStore); + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - SignatureResponse signatureResponse = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); + SignatureResponse signatureResponse = validator.from(sessionStatus, "QUALIFIED"); // Process the response (e.g., save to database or pass to another system) handleSignatureResponse(signatureResponse); diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index 4eff420e..96ae4218 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -37,6 +37,13 @@ public class SignatureResponse implements Serializable { private String endResult; private String signatureValueInBase64; private String algorithmName; + private SignatureAlgorithm signatureAlgorithm; + private HashAlgorithm hashAlgorithm; + private MaskGenAlgorithm maskGenAlgorithm; + private HashAlgorithm maskHashAlgorithm; + private int saltLength; + private TrailerField trailerField; + private FlowType flowType; private X509Certificate certificate; private String requestedCertificateLevel; private String certificateLevel; @@ -77,6 +84,62 @@ public void setAlgorithmName(String algorithmName) { this.algorithmName = algorithmName; } + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public HashAlgorithm getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + public MaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + public HashAlgorithm getMaskHashAlgorithm() { + return maskHashAlgorithm; + } + + public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { + this.maskHashAlgorithm = maskHashAlgorithm; + } + + public int getSaltLength() { + return saltLength; + } + + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + public TrailerField getTrailerField() { + return trailerField; + } + + public void setTrailerField(TrailerField trailerField) { + this.trailerField = trailerField; + } + + public FlowType getFlowType() { + return flowType; + } + + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + public X509Certificate getCertificate() { return certificate; } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseMapper.java b/src/main/java/ee/sk/smartid/SignatureResponseMapper.java deleted file mode 100644 index 8df028e1..00000000 --- a/src/main/java/ee/sk/smartid/SignatureResponseMapper.java +++ /dev/null @@ -1,201 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -public class SignatureResponseMapper { - - private static final Logger logger = LoggerFactory.getLogger(SignatureResponseMapper.class); - - /** - * Create {@link SignatureResponse} from {@link SessionStatus} - * - * @param sessionStatus session status response - * @param requestedCertificateLevel certificate level used to start the signature session - * @return the signature response - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - */ - public static SignatureResponse from(SessionStatus sessionStatus, - String requestedCertificateLevel - ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - validateSessionsStatus(sessionStatus, requestedCertificateLevel); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate certificate = sessionStatus.getCert(); - - var signatureResponse = new SignatureResponse(); - signatureResponse.setEndResult(sessionResult.getEndResult()); - signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); - signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); - signatureResponse.setCertificateLevel(certificate.getCertificateLevel()); - signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return signatureResponse; - } - - private static void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { - if (sessionStatus == null) { - throw new SmartIdClientException("Session status was not provided"); - } - - if (StringUtil.isEmpty(sessionStatus.getState())) { - throw new UnprocessableSmartIdResponseException("State parameter is missing in session status"); - } - - if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); - } - - validateSessionResult(sessionStatus, requestedCertificateLevel); - } - - private static void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel) { - SessionResult sessionResult = sessionStatus.getResult(); - - if (sessionResult == null) { - logger.error("Result is missing in the session status response"); - throw new UnprocessableSmartIdResponseException("Result is missing in the session status response"); - } - - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); - } - - if ("OK".equalsIgnoreCase(endResult)) { - logger.info("Session completed successfully"); - - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - logger.error("Document number is missing in the session result"); - throw new UnprocessableSmartIdResponseException("Document number is missing in the session result"); - } - - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - logger.error("InteractionFlowUsed is missing in the session status"); - throw new UnprocessableSmartIdResponseException("InteractionFlowUsed is missing in the session status"); - } - - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); - } - - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - validateSignature(sessionStatus); - } else { - ErrorResultHandler.handle(sessionResult); - } - } - - private static void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { - if (sessionCertificate == null || StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Missing certificate in session response"); - } - - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate level is missing in certificate"); - } - - try { - X509Certificate cert = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - cert.checkValidity(); - } catch (Exception e) { - throw new SmartIdClientException("Certificate validation failed", e); - } - - if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { - throw new CertificateLevelMismatchException(); - } - } - - private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { - CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); - CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); - - return returnedLevel.isSameLevelOrHigher(requestedLevel); - } - - private static void validateSignature(SessionStatus sessionStatus) { - String signatureProtocol = sessionStatus.getSignatureProtocol(); - - if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { - validateRawDigestSignature(sessionStatus); - } else { - throw new UnprocessableSmartIdResponseException("Unknown signature protocol: " + signatureProtocol); - } - } - - private static void validateRawDigestSignature(SessionStatus sessionStatus) { - SessionSignature signature = sessionStatus.getSignature(); - if (signature == null) { - throw new UnprocessableSmartIdResponseException("Signature object is missing"); - } - - if (StringUtil.isEmpty(signature.getValue())) { - throw new UnprocessableSmartIdResponseException("Signature value is missing"); - } - - if (StringUtil.isEmpty(signature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature algorithm is missing"); - } - - List allowedSignatureAlgorithms = Arrays.stream(SignatureAlgorithm.values()) - .map(SignatureAlgorithm::getAlgorithmName) - .toList(); - if (!allowedSignatureAlgorithms.contains(signature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + signature.getSignatureAlgorithm()); - } - - logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); - } -} diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java new file mode 100644 index 00000000..bd783499 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -0,0 +1,436 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +//TODO: review this class for possible refactoring and improvements - 2025-07-08 +public class SignatureResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); + private static final Set QUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); + private static final Set NONQUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); + private static final String QC_STATEMENT_OID = "0.4.0.1862.1.6.1"; + private static final int KEYUSAGE_NON_REPUDIATION_INDEX = 1; + + private final TrustedCACertStore trustedCaCertStore; + private final boolean qcStatementRequired; + + public SignatureResponseValidator(TrustedCACertStore store, boolean qcRequired) { + this.trustedCaCertStore = store; + this.qcStatementRequired = qcRequired; + } + + public SignatureResponseValidator(TrustedCACertStore store) { + this(store, false); + } + + /** + * Create {@link SignatureResponse} from {@link SessionStatus} + * + * @param sessionStatus session status response + * @param requestedCertificateLevel certificate level used to start the signature session + * @return the signature response + * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. + * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame + * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code + * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. + * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. + * @throws SmartIdClientException if session status is missing, incomplete or inconsistent + */ + public SignatureResponse from(SessionStatus sessionStatus, + String requestedCertificateLevel + ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + validateSessionsStatus(sessionStatus, requestedCertificateLevel); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate certificate = sessionStatus.getCert(); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()).orElse(null); + HashAlgorithm hashAlgorithm = HashAlgorithm.fromString(sessionSignature.getSignatureAlgorithmParameters().getHashAlgorithm()).orElse(null); + SessionMaskGenAlgorithm maskGenAlgorithm = sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm(); + HashAlgorithm maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()).orElse(null); + + var signatureResponse = new SignatureResponse(); + signatureResponse.setEndResult(sessionResult.getEndResult()); + signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); + signatureResponse.setSignatureAlgorithm(signatureAlgorithm); + signatureResponse.setHashAlgorithm(hashAlgorithm); + signatureResponse.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + signatureResponse.setMaskHashAlgorithm(maskGenHashAlgorithm); + signatureResponse.setSaltLength(sessionSignature.getSignatureAlgorithmParameters().getSaltLength()); + signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + signatureResponse.setTrailerField(TrailerField.OXBC); + + signatureResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); + signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); + signatureResponse.setCertificateLevel(certificate.getCertificateLevel()); + signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return signatureResponse; + } + + private void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Session status was not provided"); + } + + if (StringUtil.isEmpty(sessionStatus.getState())) { + throw new UnprocessableSmartIdResponseException("State parameter is missing in session status"); + } + + if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); + } + + validateSessionResult(sessionStatus, requestedCertificateLevel); + } + + private void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel) { + SessionResult sessionResult = sessionStatus.getResult(); + + if (sessionResult == null) { + logger.error("Result is missing in the session status response"); + throw new UnprocessableSmartIdResponseException("Result is missing in the session status response"); + } + + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); + } + + if ("OK".equalsIgnoreCase(endResult)) { + logger.info("Session completed successfully"); + + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Document number is missing in the session result"); + } + + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("InteractionFlowUsed is missing in the session status"); + } + + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); + } + + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + validateSignature(sessionStatus); + } else { + ErrorResultHandler.handle(sessionResult); + } + } + + private void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { + if (sessionCertificate == null || StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Missing certificate in session response"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate level is missing in certificate"); + } + + X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); + + if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { + throw new CertificateLevelMismatchException(); + } + + validateCertificatePoliciesAndPurpose(certificate); + validateCertificateChain(certificate); + } + + private void validateCertificatePoliciesAndPurpose(X509Certificate cert) { + Set oids = getPolicyOids(cert); + boolean hasAllQualified = oids.containsAll(QUALIFIED_POLICY_OIDS); + boolean hasAllNonQual = oids.containsAll(NONQUALIFIED_POLICY_OIDS); + if (!hasAllQualified && !hasAllNonQual) { + throw new UnprocessableSmartIdResponseException("CertificatePolicies missing required Smart-ID OIDs"); + } + + boolean[] keyUsage = cert.getKeyUsage(); + if (keyUsage == null || keyUsage.length < 2 || !keyUsage[KEYUSAGE_NON_REPUDIATION_INDEX]) { + throw new UnprocessableSmartIdResponseException("KeyUsage must contain NonRepudiation"); + } + + if (qcStatementRequired && !containsQcStatement(cert)) { + throw new UnprocessableSmartIdResponseException("QCStatement 0.4.0.1862.1.6.1 missing"); + } + } + + private void validateCertificateChain(X509Certificate certificate) { + try { + var x509CertSelector = new X509CertSelector(); + x509CertSelector.setCertificate(certificate); + + var params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), x509CertSelector); + + CertStore intermediates = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); + params.addCertStore(intermediates); + params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); + + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); + + logger.debug("Signature certificate validated. Trust anchor: {}", result.getTrustAnchor().getTrustedCert().getSubjectX500Principal()); + + } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { + throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); + } + } + + private static X509Certificate parseAndCheckCertificate(String certBase64) { + X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Signature certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Signature certificate is invalid", ex); + } + return certificate; + } + + private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { + CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); + CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); + + return returnedLevel.isSameLevelOrHigher(requestedLevel); + } + + private static Set getPolicyOids(X509Certificate certificate) { + Set result = new HashSet<>(); + byte[] extensionValue = certificate.getExtensionValue("2.5.29.32"); + if (extensionValue == null) return result; + try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { + ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); + try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { + CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); + for (PolicyInformation pi : policies.getPolicyInformation()) { + result.add(pi.getPolicyIdentifier().getId()); + } + } + } catch (Exception e) { + logger.debug("Unable to parse CertificatePolicies", e); + } + return result; + } + + private static boolean containsQcStatement(X509Certificate cert) { + byte[] extensionValue = cert.getExtensionValue("1.3.6.1.5.5.7.1.3"); + if (extensionValue == null) return false; + try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { + ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); + try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { + ASN1Sequence seq = (ASN1Sequence) ais2.readObject(); + for (int i = 0; i < seq.size(); i++) { + QCStatement st = QCStatement.getInstance(seq.getObjectAt(i)); + if (QC_STATEMENT_OID.equals(st.getStatementId().getId())) return true; + } + } + } catch (Exception ex) { + logger.debug("Unable to parse QCStatements", ex); + } + return false; + } + + private static void validateSignature(SessionStatus sessionStatus) { + String signatureProtocol = sessionStatus.getSignatureProtocol(); + + if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { + validateRawDigestSignature(sessionStatus); + } else { + throw new UnprocessableSmartIdResponseException("Unknown signature protocol: " + signatureProtocol); + } + } + + private static void validateRawDigestSignature(SessionStatus sessionStatus) { + SessionSignature signature = sessionStatus.getSignature(); + if (signature == null) { + throw new UnprocessableSmartIdResponseException("Signature object is missing"); + } + + validateSignatureValue(signature.getValue()); + validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); + validateFlowType(signature.getFlowType()); + validateSignatureAlgorithm(signature.getSignatureAlgorithm()); + validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); + + logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); + } + + private static void validateSignatureValue(String value) { + if (StringUtil.isEmpty(value) || !BASE64_PATTERN.matcher(value).matches()) { + throw new UnprocessableSmartIdResponseException("Signature value is missing or not Base64"); + } + } + + private static void validateSignatureAlgorithmName(String algorithm) { + if (StringUtil.isEmpty(algorithm)) { + throw new UnprocessableSmartIdResponseException("Signature algorithm is missing"); + } + + List allowedSignatureAlgorithms = Arrays.stream(SignatureAlgorithm.values()) + .map(SignatureAlgorithm::getAlgorithmName) + .toList(); + + if (!allowedSignatureAlgorithms.contains(algorithm)) { + throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + algorithm + ); + } + } + + private static void validateFlowType(String flowType) { + if (StringUtil.isEmpty(flowType)) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.flowType` is empty"); + } + if (!FlowType.isSupported(flowType)) { + logger.error("Invalid `signature.flowType` in session status: {}", flowType); + throw new UnprocessableSmartIdResponseException("Invalid `signature.flowType` in session status"); + } + } + + private static void validateSignatureAlgorithm(String algorithm) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(algorithm).orElse(null); + if (signatureAlgorithm != SignatureAlgorithm.RSASSA_PSS) { + throw new UnprocessableSmartIdResponseException("signatureAlgorithm must be rsassa-pss"); + } + } + + private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { + if (sessionSignatureAlgorithmParameters == null) { + throw new UnprocessableSmartIdResponseException("SignatureAlgorithmParameters is missing"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status"); + } + + var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + } + + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status"); + } + + if (maskGenAlgorithm.getParameters() == null || StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' is empty"); + } + + Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (mgfHashAlgorithm.isEmpty()) { + logger.error("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status"); + } + + if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { + logger.error("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm': expected {}, got {}", + hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm'"); + } + + if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); + } + + int expectedSaltLength = hashAlgorithm.get().getOctetLength(); + int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); + + if (expectedSaltLength != actualSaltLength) { + logger.error("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status: expected {}, got {}", expectedSaltLength, actualSaltLength); + throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + } + + if (!TrailerField.OXBC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { + logger.error("Invalid `signature.signatureAlgorithmParameters.trailerField` in session status: {}", sessionSignatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status"); + } + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java b/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java deleted file mode 100644 index 097eac0e..00000000 --- a/src/test/java/ee/sk/smartid/SignatureResponseMapperTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; - -class SignatureResponseMapperTest { - - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - - @Test - void from_stateParameterMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setState(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("State parameter is missing in session status", ex.getMessage()); - } - - @Test - void from_sessionNotComplete() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setState("RUNNING"); - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertTrue(ex.getMessage().contains("Session is not complete")); - } - - @Test - void from_sessionResultNull() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("Result is missing in the session status response", ex.getMessage()); - } - - @Test - void from_endResultParameterMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getResult().setEndResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("End result parameter is missing in the session result", ex.getMessage()); - } - - @Test - void from_missingDocumentNumber() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getResult().setDocumentNumber(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("Document number is missing in the session result", ex.getMessage()); - } - - @Test - void from_missingInteractionFlowUsed() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setInteractionTypeUsed(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); - } - - @Test - void from_signatureProtocolMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setSignatureProtocol(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature protocol is missing in session status", ex.getMessage()); - } - - @Nested - class CertificateValidation { - - @Test - void from_missingCertificate() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setCert(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("Missing certificate in session response", ex.getMessage()); - } - - @Test - void from_missingCertificateValue() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getCert().setValue(null); - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("Missing certificate in session response", ex.getMessage()); - } - - @Test - void from_certificateLevelMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getCert().setCertificateLevel(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Certificate level is missing in certificate", ex.getMessage()); - } - - @Test - void from_certificateLevelMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getCert().setCertificateLevel("ADVANCED"); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - } - - @Nested - class SignatureValidation { - - @Test - void from_validRawDigestSignature() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = SignatureResponseMapper.from(sessionStatus, "QUALIFIED"); - assertEquals("OK", response.getEndResult()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void from_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = SignatureResponseMapper.from(sessionStatus, certificateLevel.name()); - assertEquals("OK", response.getEndResult()); - assertEquals("QUALIFIED", response.getCertificateLevel()); - } - - @Test - void from_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); - } - - @Test - void from_unknownSignatureProtocol() { - SessionStatus sessionStatus = createMockSessionStatus("UNKNOWN_PROTOCOL", "sha512WithRSAEncryption"); - sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - - assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void from_handleSessionEndResultErrors(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(expectedException, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - } - - @Test - void from_sessionStatusNull() { - - var ex = assertThrows(SmartIdClientException.class, () -> SignatureResponseMapper.from(null, "QUALIFIED")); - - assertEquals("Session status was not provided", ex.getMessage()); - } - - @Test - void from_signatureMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.setSignature(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature object is missing", ex.getMessage()); - } - - @Test - void from_signatureValueMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getSignature().setValue(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature value is missing", ex.getMessage()); - } - - @Test - void from_signatureAlgorithmMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "sha512WithRSAEncryption"); - sessionStatus.getSignature().setSignatureAlgorithm(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> SignatureResponseMapper.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature algorithm is missing", ex.getMessage()); - } - } - - private static SessionStatus createMockSessionStatus(String signatureProtocol, String signatureAlgorithm) { - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(getEncodedCertificateData()); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("expectedDigest"); - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setServerRandom("serverRandomValue"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(signatureProtocol); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - return sessionStatus; - } - - private static String getEncodedCertificateData() { - return SignatureResponseMapperTest.SIGN_CERT.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); - } -} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java new file mode 100644 index 00000000..36af0b8d --- /dev/null +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -0,0 +1,519 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class SignatureResponseValidatorTest { + + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + + private SignatureResponseValidator signatureResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .build(); + signatureResponseValidator = new SignatureResponseValidator(trustedCaCertStore); + } + + @Test + void from_stateParameterMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("State parameter is missing in session status", ex.getMessage()); + } + + @Test + void from_sessionNotComplete() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState("RUNNING"); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertTrue(ex.getMessage().contains("Session is not complete")); + } + + @Test + void from_sessionResultNull() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Result is missing in the session status response", ex.getMessage()); + } + + @Test + void from_sessionResultNull_throwsException() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + + sessionStatus.setResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Result is missing in the session status response", ex.getMessage()); + } + + @Test + void from_missingDocumentNumber() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getResult().setDocumentNumber(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Document number is missing in the session result", ex.getMessage()); + } + + @Test + void from_missingInteractionFlowUsed() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setInteractionTypeUsed(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); + } + + @Test + void from_signatureProtocolMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature protocol is missing in session status", ex.getMessage()); + } + + @Nested + class CertificateValidation { + + @Test + void from_missingCertificate() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setCert(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Missing certificate in session response", ex.getMessage()); + } + + @Test + void from_missingCertificateValue() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setValue(null); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Missing certificate in session response", ex.getMessage()); + } + + @Test + void from_certificateLevelMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Certificate level is missing in certificate", ex.getMessage()); + } + + @Test + void from_certificateLevelMismatch() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel("ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void from_qcStatementRequiredButMissing() { + var validatorWithQcRequired = new SignatureResponseValidator(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(), true); + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validatorWithQcRequired.from(sessionStatus, "QUALIFIED")); + + assertEquals("QCStatement 0.4.0.1862.1.6.1 missing", ex.getMessage()); + } + } + + @Nested + class SignatureValidation { + + @Test + void from_validRawDigestSignature() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.from(sessionStatus, "QUALIFIED"); + assertEquals("OK", response.getEndResult()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void from_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.from(sessionStatus, certificateLevel.name()); + assertEquals("OK", response.getEndResult()); + assertEquals("QUALIFIED", response.getCertificateLevel()); + } + + @Test + void from_rawDigestUnexpectedAlgorithm() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); + } + + @Test + void from_unknownSignatureProtocol() { + SessionStatus sessionStatus = createMockSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_handleSessionEndResultErrors(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(expectedException, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + } + + @Test + void from_endResultMissing_throwsException() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + sessionStatus.getResult().setEndResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("End result parameter is missing in the session result", ex.getMessage()); + } + + @Test + void from_sessionStatusNull() { + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(null, "QUALIFIED")); + assertEquals("Session status was not provided", ex.getMessage()); + } + + @Test + void from_signatureMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignature(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature object is missing", ex.getMessage()); + } + + @Test + void from_signatureValueMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature value is missing or not Base64", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature algorithm is missing", ex.getMessage()); + } + + @Test + void from_flowTypeMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field `signature.flowType` is empty", ex.getMessage()); + } + + @Test + void from_invalidFlowType() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Invalid `signature.flowType` in session status", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmNotSupported() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); + } + + @Test + void from_signatureAlgorithmNotRsassaPss() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); + sessionStatus.getSignature().setSignatureAlgorithm("rsa"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Unexpected signature algorithm. Expected one of: [rsassa-pss], but got: rsa", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersHashAlgorithmMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersInvalidHashAlgorithm() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithmParameters(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("SignatureAlgorithmParameters is missing", ex.getMessage()); + } + + @Test + void from_invalidMaskGenAlgorithmName() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersMaskGenAlgorithmMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersMaskGenAlgorithmAlgorithmEmpty() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersMaskGenHashAlgorithmEmpty() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersMaskGenHashAlgorithmInvalid() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status", ex.getMessage()); + } + + @Test + void from_mismatchedHashAlgorithms() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm'", ex.getMessage()); + } + + @Test + void from_signatureAlgorithmParametersSaltLengthMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); + } + + @Test + void from_invalidSaltLength() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + + assertEquals("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); + } + + @Test + void from_invalidTrailerField() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status", ex.getMessage()); + } + } + + private static SessionStatus createMockSessionStatus(String signatureProtocol, String signatureAlgorithm) { + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(getEncodedCertificateData()); + + var params = new SessionSignatureAlgorithmParameters(); + params.setHashAlgorithm("SHA-512"); + var mgf = new SessionMaskGenAlgorithm(); + mgf.setAlgorithm("id-mgf1"); + var mgfParams = new SessionMaskGenAlgorithmParameters(); + mgfParams.setHashAlgorithm("SHA-512"); + mgf.setParameters(mgfParams); + params.setMaskGenAlgorithm(mgf); + params.setSaltLength(64); + params.setTrailerField("0xbc"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("expectedDigest"); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(params); + sessionSignature.setServerRandom("serverRandomValue"); + sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); + sessionSignature.setFlowType("QR"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(signatureProtocol); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static String getEncodedCertificateData() { + return SignatureResponseValidatorTest.SIGN_CERT.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 9787b7e7..8e5ffd34 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -30,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -65,12 +64,11 @@ import ee.sk.smartid.FileTrustedCAStoreBuilder; import ee.sk.smartid.HashType; import ee.sk.smartid.QrCodeGenerator; -import ee.sk.smartid.QrCodeUtil; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; import ee.sk.smartid.SignableData; import ee.sk.smartid.SignatureResponse; -import ee.sk.smartid.SignatureResponseMapper; +import ee.sk.smartid.SignatureResponseValidator; import ee.sk.smartid.SmartIdClient; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.TrustedCACertStore; @@ -86,7 +84,6 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; - @Disabled("Replace relying party UUID and name with your own values in setup") @SmartIdDemoIntegrationTest public class ReadmeIntegrationTest { @@ -381,7 +378,10 @@ void signature_withDocumentNumberAndQRCode() { // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) assertEquals("COMPLETE", signatureSessionStatus.getState()); - SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); + SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); @@ -470,7 +470,10 @@ void signature_withSemanticIdentifier() { // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) assertEquals("COMPLETE", signatureSessionStatus.getState()); - SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); + SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); @@ -667,7 +670,10 @@ void signature_withSemanticsIdentifier() { // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) assertEquals("COMPLETE", signatureSessionStatus.getState()); - SignatureResponse signatureResponse = SignatureResponseMapper.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); + SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOEE-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); From 64fbc24af1680300d9d44b9a870662f682b7468d Mon Sep 17 00:00:00 2001 From: ragnarhaide <128774709+ragnarhaide@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:31:48 +0300 Subject: [PATCH 29/57] =?UTF-8?q?SLIB-101=20-=20replaced=20dynamic-link=20?= =?UTF-8?q?certificate=20choice=20with=20device-link=20=E2=80=A6=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SLIB-101 - replaced dynamic-link certificate choice with device-link certificate choice * SLIB-101 - added response validations, added tests, README update * SLIB-101 - fixes #120, renamed initialCallbackURL to initialCallbackUrl * SLIB-101 - refactored response validations --------- Co-authored-by: ragnar.haide --- CHANGELOG.md | 11 +- README.md | 30 +- .../AuthenticationResponseValidator.java | 2 +- ...nkAuthenticationSessionRequestBuilder.java | 18 +- ...rtificateChoiceSessionRequestBuilder.java} | 87 +++-- ...iceLinkSignatureSessionRequestBuilder.java | 18 +- .../java/ee/sk/smartid/SmartIdClient.java | 4 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 6 +- .../sk/smartid/rest/SmartIdRestConnector.java | 12 +- .../dao/AuthenticationSessionRequest.java | 2 +- .../dao/CertificateChoiceSessionRequest.java | 11 + .../rest/dao/SignatureSessionRequest.java | 10 +- ...thenticationSessionRequestBuilderTest.java | 12 +- ...ficateChoiceSessionRequestBuilderTest.java | 305 ++++++++++++++++++ ...inkSignatureSessionRequestBuilderTest.java | 10 +- ...ficateChoiceSessionRequestBuilderTest.java | 204 ------------ .../java/ee/sk/smartid/SmartIdClientTest.java | 54 +++- .../integration/ReadmeIntegrationTest.java | 2 +- .../SmartIdRestIntegrationTest.java | 6 +- .../rest/SmartIdRestConnectorTest.java | 51 ++- ...-certificate-choice-session-response.json} | 1 + 21 files changed, 522 insertions(+), 334 deletions(-) rename src/main/java/ee/sk/smartid/{DynamicLinkCertificateChoiceSessionRequestBuilder.java => DeviceLinkCertificateChoiceSessionRequestBuilder.java} (55%) create mode 100644 src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java delete mode 100644 src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java rename src/test/resources/responses/{dynamic-link-certificate-choice-session-response.json => device-link-certificate-choice-session-response.json} (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 327e793e..959d798a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.7] - 2025-07-10 + +- Renamed dynamic-link certificate choice to device-link certificate choice. +- Updated certificate choice endpoint to use /device-link/ paths. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. + ## [3.1.6] - 2025-07-08 ### Added @@ -28,7 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Replaced signature algorithm list with fixed `rsassa-pss`. - Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. - Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackURL` field with regex validation. +- Added `initialCallbackUrl` field with regex validation. - Added `deviceLinkBase` to session response. ## [3.1.4] - 2025-07-05 @@ -84,7 +91,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Replaced signature algorithm list with fixed `rsassa-pss`. - Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. - Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackURL` field with regex validation. +- Added `initialCallbackUrl` field with regex validation. - Added `deviceLinkBase` to session response. ## [3.1] - 2025-05-20 diff --git a/README.md b/README.md index 7640d6f8..f77136ce 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ This library supports Smart-ID API v3.1. * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) * [Initiating a dynamic-link authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) - * [Dynamic link certificate choice session](#dynamic-link-certificate-choice-session) - * [Examples of initiating a dynamic-link certificate choice session](#examples-of-initiating-a-dynamic-link-certificate-choice-session) - * [Initiating dynamic-link certificate choice](#initiating-an-anonymous-certificate-choice-session) + * [Device link certificate choice session](#device-link-certificate-choice-session) + * [Examples of initiating a device-link certificate choice session](#examples-of-initiating-a-device-link-certificate-choice-session) + * [Initiating device-link certificate choice](#initiating-an-anonymous-certificate-choice-session) * [Device-link signature session](#device-link-signature-session) * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) @@ -175,7 +175,7 @@ More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/ * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -* `initialCallbackURL`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. #### Response parameters @@ -321,11 +321,11 @@ Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Dynamic-link certificate choice session -!!!Dynamic-link Certificate Choice session Cannot be used at the moment!!! +### Device-link certificate choice session -The Smart-ID API v3.1 introduces dynamic-link certificate choice session. This allows more secure way of initiating signing. -Scanning QR-code or clicking on dynamic link will prove that the certificates of the device being used for signing is in the proximity where the signing was initiated. +The Smart-ID API v3.1 introduces device-link certificate choice session. This allows more secure way of initiating signing. +Scanning QR-code or clicking on device link will prove that the certificates of the device being used for signing is in the proximity where the signing was initiated. +The certificate choice session must be followed by a linked notification-based signature session. #### Request Parameters @@ -335,21 +335,24 @@ Scanning QR-code or clicking on dynamic link will prove that the certificates of * `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. * `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. +* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. #### Response parameters * `sessionID`: A string that can be used to request the session status result. * `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). * `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. -#### Examples of initiating a dynamic-link certificate choice session +#### Examples of initiating a device-link certificate choice session ##### Initiating an anonymous certificate choice session ```java -DynamicLinkSessionResponse certificateChoice = client.createDynamicLinkCertificateRequest() +DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() .withRelyingPartyUUID(client.getRelyingPartyUUID()) .withRelyingPartyName(client.getRelyingPartyName()) .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) .initiateCertificateChoice(); String sessionId = certificateChoice.getSessionID(); @@ -358,9 +361,10 @@ String sessionId = certificateChoice.getSessionID(); String sessionToken = certificateChoice.getSessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = certificateChoice.getSessionSecret(); +String deviceLinkBase = certificateChoice.getDeviceLinkBase(); Instant responseReceivedAt = certificateChoice.getReceivedAt(); ``` -Jump to [Generate QR-code and dynamic link](#generating-qr-code-or-device-link) to see how to generate QR-code or dynamic link from the response. +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. ### Device-link signature session @@ -382,7 +386,7 @@ The request parameters for the device-link signature session are as follows: * Each interaction object includes: * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `initialCallbackURL`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. * `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. * `requestProperties`: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. @@ -420,7 +424,7 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withSemanticsIdentifier(semanticsIdentifier) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .withInitialCallbackURL("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) .initSignatureSession(); // Process the signature response diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index 36ee6155..fd758a88 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -208,7 +208,7 @@ private byte[] constructPayload(AuthenticationResponse authenticationResponse, StringUtil.isEmpty(brokeredRpName) ? "" : Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)), Base64.getEncoder().encodeToString(calculateInteractionsDigest(authenticationSessionRequest)), authenticationResponse.getInteractionTypeUsed(), - StringUtil.orEmpty(authenticationSessionRequest.initialCallbackURL()), + StringUtil.orEmpty(authenticationSessionRequest.initialCallbackUrl()), authenticationResponse.getFlowType().getDescription() }; return String diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index 80392cdc..9bf2a9aa 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -69,7 +69,7 @@ public class DeviceLinkAuthenticationSessionRequestBuilder { private Set capabilities; private SemanticsIdentifier semanticsIdentifier; private String documentNumber; - private String initialCallbackURL; + private String initialCallbackUrl; private AuthenticationSessionRequest authenticationSessionRequest; @@ -222,11 +222,11 @@ public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String d *

* The callback URL should be set when using same device flows (like Web2App or App2App). * - * @param initialCallbackURL the initial callback URL + * @param initialCallbackUrl the initial callback URL * @return this builder */ - public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackURL(String initialCallbackURL) { - this.initialCallbackURL = initialCallbackURL; + public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; return this; } @@ -291,7 +291,7 @@ private void validateRequestParameters() { } validateSignatureParameters(); validateInteractions(); - validateInitialCallbackURL(); + validateInitialCallbackUrl(); } private void validateSignatureParameters() { @@ -328,9 +328,9 @@ private void validateInteractions() { interactions.forEach(DeviceLinkInteraction::validate); } - private void validateInitialCallbackURL() { - if (!StringUtil.isEmpty(initialCallbackURL) && !initialCallbackURL.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("initialCallbackURL must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } @@ -348,7 +348,7 @@ private AuthenticationSessionRequest createAuthenticationRequest() { DeviceLinkUtil.encodeToBase64(interactions), this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, capabilities, - initialCallbackURL + initialCallbackUrl ); } diff --git a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java similarity index 55% rename from src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java rename to src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index c6f131e1..4dcc2240 100644 --- a/src/main/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -39,10 +39,12 @@ import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.util.StringUtil; -public class DynamicLinkCertificateChoiceSessionRequestBuilder { +public class DeviceLinkCertificateChoiceSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(DynamicLinkCertificateChoiceSessionRequestBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(DeviceLinkCertificateChoiceSessionRequestBuilder.class); + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; private final SmartIdConnector connector; private String relyingPartyUUID; @@ -51,13 +53,14 @@ public class DynamicLinkCertificateChoiceSessionRequestBuilder { private String nonce; private Set capabilities; private Boolean shareMdClientIpAddress; + private String initialCallbackUrl; /** - * Constructs a new DynamicLinkCertificateRequestBuilder with the given Smart-ID connector + * Constructs a new DeviceLinkCertificateRequestBuilder with the given Smart-ID connector * * @param connector the Smart-ID connector */ - public DynamicLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + public DeviceLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { this.connector = connector; } @@ -67,7 +70,7 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connec * @param relyingPartyUUID the relying party UUID * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { this.relyingPartyUUID = relyingPartyUUID; return this; } @@ -78,7 +81,7 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(St * @param relyingPartyName the relying party name * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { this.relyingPartyName = relyingPartyName; return this; } @@ -89,7 +92,7 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(St * @param certificateLevel the certificate level * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; return this; } @@ -100,7 +103,7 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(Ce * @param nonce the nonce * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { this.nonce = nonce; return this; } @@ -111,7 +114,7 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) * @param capabilities the capabilities * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { this.capabilities = Set.of(capabilities); return this; } @@ -122,32 +125,42 @@ public DynamicLinkCertificateChoiceSessionRequestBuilder withCapabilities(String * @param shareMdClientIpAddress whether to share the Mobile device IP address * @return this builder */ - public DynamicLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + public DeviceLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { this.shareMdClientIpAddress = shareMdClientIpAddress; return this; } /** - * Starts a dynamic link-based certificate choice session and returns the session response. - * This response includes essential values such as sessionID, sessionToken, and sessionSecret, + * Sets the initial callback URL for the device link session. + * This URL is used to redirect the user after the session is initialized. + * + * @param initialCallbackUrl the initial callback URL + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Starts a device link-based certificate choice session and returns the session response. + * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, * which can be used by the Relying Party to manage and verify the session independently. *

* - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, and sessionSecret for further session management. + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. * @throws SmartIdClientException if the response is invalid or missing necessary session data. + * @throws UnprocessableSmartIdResponseException if the response is missing required fields. */ public DeviceLinkSessionResponse initCertificateChoice() { - validateParameters(); - CertificateChoiceSessionRequest request = createCertificateRequest(); - DeviceLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); - - if (response == null || response.getSessionID() == null) { - throw new UnprocessableSmartIdResponseException("Dynamic link certificate choice session failed: invalid response received."); - } - return response; + validateRequestParameters(); + CertificateChoiceSessionRequest certificateChoiceSessionRequest = createCertificateRequest(); + DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(certificateChoiceSessionRequest); + validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); + return deviceLinkCertificateChoiceSessionResponse; } - private void validateParameters() { + private void validateRequestParameters() { if (isEmpty(relyingPartyUUID)) { logger.error("Parameter relyingPartyUUID must be set"); throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); @@ -159,6 +172,7 @@ private void validateParameters() { if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { throw new SmartIdClientException("Nonce must be between 1 and 30 characters"); } + validateInitialCallbackUrl(); } private CertificateChoiceSessionRequest createCertificateRequest() { @@ -177,7 +191,36 @@ private CertificateChoiceSessionRequest createCertificateRequest() { var requestProperties = new RequestProperties(this.shareMdClientIpAddress); request.setRequestProperties(requestProperties); } + request.setInitialCallbackUrl(initialCallbackUrl); return request; } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionID())) { + logger.error("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionToken())) { + logger.error("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionSecret())) { + logger.error("Session secret is missing from the response"); + throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); + } + + if (deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase() == null || deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase().toString().isBlank()) { + logger.error("deviceLinkBase is missing or empty in the response"); + throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing or empty in the response"); + } + } } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index f018686c..30a910b2 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -68,7 +68,7 @@ public class DeviceLinkSignatureSessionRequestBuilder { private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; private SignableData signableData; private SignableHash signableHash; - private String initialCallbackURL; + private String initialCallbackUrl; /** * Constructs a new Smart-ID signature request builder with the given connector. @@ -234,11 +234,11 @@ public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash si *

* This URL is used to redirect the user after the signature session is completed. * - * @param initialCallbackURL the initial callback URL + * @param initialCallbackUrl the initial callback URL * @return this builder instance */ - public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackURL(String initialCallbackURL) { - this.initialCallbackURL = initialCallbackURL; + public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; return this; } @@ -302,7 +302,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { request.setRequestProperties(requestProperties); } request.setCapabilities(capabilities); - request.setInitialCallbackURL(initialCallbackURL); + request.setInitialCallbackUrl(initialCallbackUrl); return request; } @@ -314,7 +314,7 @@ private void validateParameters() { throw new SmartIdClientException("Relying Party Name must be set."); } validateInteractions(); - validateInitialCallbackURL(); + validateInitialCallbackUrl(); if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); @@ -330,9 +330,9 @@ private void validateInteractions() { interactions.forEach(DeviceLinkInteraction::validate); } - private void validateInitialCallbackURL() { - if (!StringUtil.isEmpty(initialCallbackURL) && !initialCallbackURL.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("initialCallbackURL must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 240ef94e..32b4f274 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -72,8 +72,8 @@ public class SmartIdClient { * * @return a builder for creating a new device link certificate choice session request */ - public DynamicLinkCertificateChoiceSessionRequestBuilder createDynamicLinkCertificateRequest() { - return new DynamicLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) .withRelyingPartyUUID(relyingPartyUUID) .withRelyingPartyName(relyingPartyName); } diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index cca90805..b92ea262 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -64,12 +64,12 @@ public interface SmartIdConnector extends Serializable { void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); /** - * Initiates a dynamic link based certificate choice request. + * Initiates a device link based certificate choice request. * * @param request CertificateChoiceSessionRequest containing necessary parameters - * @return DynamicLinkSessionResponse containing sessionID, sessionToken, and sessionSecret + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request); + DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoiceSessionRequest request); /** * Initiates a notification based certificate choice request. diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index cfa15217..d3244db6 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -78,7 +78,7 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); private static final String SESSION_STATUS_URI = "/session/{sessionId}"; - private static final String CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH = "/certificatechoice/dynamic-link/anonymous"; + private static final String CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "/certificatechoice/device-link/anonymous"; private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; @@ -180,14 +180,14 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public DeviceLinkSessionResponse initDynamicLinkCertificateChoice(CertificateChoiceSessionRequest request) { - logger.debug("Initiating dynamic link based certificate choice request"); + public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoiceSessionRequest request) { + logger.debug("Initiating device link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) - .path(CERTIFICATE_CHOICE_DYNAMIC_LINK_PATH) + .path(CERTIFICATE_CHOICE_DEVICE_LINK_PATH) .build(); - return postDynamicLinkCertificateChoiceRequest(uri, request); + return postDeviceLinkCertificateChoiceRequest(uri, request); } @Override @@ -322,7 +322,7 @@ private NotificationAuthenticationSessionResponse postNotificationAuthentication } } - private DeviceLinkSessionResponse postDynamicLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { + private DeviceLinkSessionResponse postDeviceLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { try { return postRequest(uri, request, DeviceLinkSessionResponse.class); } catch (NotFoundException ex) { diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java index 12bcb09a..7d228f0b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -40,5 +40,5 @@ public record AuthenticationSessionRequest(String relyingPartyUUID, String interactions, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackURL) implements Serializable { + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java index e8861e5e..de256abb 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java @@ -48,6 +48,9 @@ public class CertificateChoiceSessionRequest implements Serializable { @JsonInclude(JsonInclude.Include.NON_NULL) private RequestProperties requestProperties; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String initialCallbackUrl; + public String getRelyingPartyUUID() { return relyingPartyUUID; } @@ -95,4 +98,12 @@ public RequestProperties getRequestProperties() { public void setRequestProperties(RequestProperties requestProperties) { this.requestProperties = requestProperties; } + + public String getInitialCallbackUrl() { + return initialCallbackUrl; + } + + public void setInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java index 5eb10da1..63a510f0 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java @@ -57,7 +57,7 @@ public class SignatureSessionRequest implements Serializable { private RequestProperties requestProperties; @JsonInclude(JsonInclude.Include.NON_NULL) - private String initialCallbackURL; + private String initialCallbackUrl; public String getRelyingPartyUUID() { return relyingPartyUUID; @@ -127,11 +127,11 @@ public void setRequestProperties(RequestProperties requestProperties) { this.requestProperties = requestProperties; } - public String getInitialCallbackURL() { - return initialCallbackURL; + public String getInitialCallbackUrl() { + return initialCallbackUrl; } - public void setInitialCallbackURL(String initialCallbackURL) { - this.initialCallbackURL = initialCallbackURL; + public void setInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 19cbf644..59924868 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -233,14 +233,14 @@ void initAuthenticationSession_initialCallbackUrlIsValid_ok() { .withRpChallenge(generateBase64String("a".repeat(32))) .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withInitialCallbackURL("https://valid.example.com/path") + .withInitialCallbackUrl("https://valid.example.com/path") .initAuthenticationSession(); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); AuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals("https://valid.example.com/path", request.initialCallbackURL()); + assertEquals("https://valid.example.com/path", request.initialCallbackUrl()); } @ParameterizedTest @@ -360,7 +360,7 @@ void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String .withRpChallenge(generateBase64String("a".repeat(32))) .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log in"))) - .withInitialCallbackURL(url) + .withInitialCallbackUrl(url) .initAuthenticationSession() ); assertEquals(expectedErrorMessage, exception.getMessage()); @@ -617,9 +617,9 @@ private static class InvalidInitialCallbackUrlArgumentProvider implements Argume @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("http://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") ); } } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java new file mode 100644 index 00000000..ec6f9da6 --- /dev/null +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -0,0 +1,305 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; + +class DeviceLinkCertificateChoiceSessionRequestBuilderTest { + + private SmartIdConnector connector; + private DeviceLinkCertificateChoiceSessionRequestBuilder builderService; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + + builderService = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withInitialCallbackUrl("https://example.com/callback"); + } + + @Test + void initiateCertificateChoice() { + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullRequestProperties() { + builderService.withShareMdClientIpAddress(false); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_missingCertificateLevel() { + builderService.withCertificateLevel(null); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_withValidCapabilities() { + builderService.withCapabilities("ADVANCED", "QUALIFIED"); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullCapabilities() { + builderService.withCapabilities(); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.getSessionID()); + assertEquals("test-session-token", result.getSessionToken()); + assertEquals("test-session-secret", result.getSessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { + var responseWithNullSessionID = new DeviceLinkSessionResponse(); + responseWithNullSessionID.setSessionID(sessionId); + responseWithNullSessionID.setSessionToken("test-session-token"); + responseWithNullSessionID.setSessionSecret("test-session-secret"); + responseWithNullSessionID.setDeviceLinkBase(URI.create("https://example.com/device-link")); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Session ID is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken(sessionToken); + response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Session token is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret(sessionSecret); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Session secret is missing from the response", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(uriString == null ? null : URI.create(uriString)); + + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("deviceLinkBase is missing or empty in the response", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_userAccountNotFound() { + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); + + var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); + assertEquals(UserAccountNotFoundException.class, ex.getClass()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyUUID() { + builderService.withRelyingPartyUUID(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyName() { + builderService.withRelyingPartyName(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_invalidNonce() { + builderService.withNonce("1234567890123456789012345678901"); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_emptyNonce() { + builderService.withNonce(""); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_withoutInitialCallbackUrl() { + builderService.withInitialCallbackUrl(null); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullNonce() { + builderService.withNonce(null); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce("123456") + .withInitialCallbackUrl(url); + + var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + } + + private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { + var response = new DeviceLinkSessionResponse(); + response.setSessionID("test-session-id"); + response.setSessionToken("test-session-token"); + response.setSessionSecret("test-session-secret"); + response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + return response; + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index fa4c45fb..f2713424 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -80,7 +80,7 @@ void setUp() { .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())) - .withInitialCallbackURL("https://example.com/callback"); + .withInitialCallbackUrl("https://example.com/callback"); } @Test @@ -314,7 +314,7 @@ void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url, .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in"))) - .withInitialCallbackURL(url) + .withInitialCallbackUrl(url) .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) .initSignatureSession() ); @@ -490,9 +490,9 @@ private static class InvalidInitialCallbackUrlArgumentProvider implements Argume @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("http://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "initialCallbackURL must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") ); } } diff --git a/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java deleted file mode 100644 index 02e498f1..00000000 --- a/src/test/java/ee/sk/smartid/DynamicLinkCertificateChoiceSessionRequestBuilderTest.java +++ /dev/null @@ -1,204 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; - -class DynamicLinkCertificateChoiceSessionRequestBuilderTest { - - private SmartIdConnector connector; - private DynamicLinkCertificateChoiceSessionRequestBuilder builderService; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - - builderService = new DynamicLinkCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890"); - } - - @Test - void initiateCertificateChoice() { - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - - verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullRequestProperties() { - builderService.withShareMdClientIpAddress(false); - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - - verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_missingCertificateLevel() { - builderService.withCertificateLevel(null); - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_withValidCapabilities() { - builderService.withCapabilities("ADVANCED", "QUALIFIED"); - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - - verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullCapabilities() { - builderService.withCapabilities(); - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - - verify(connector).initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); - } - - @Nested - class ErrorCases { - - @Test - void initiateCertificateChoice_whenResponseIsNull() { - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_whenSessionIDIsNull() { - var responseWithNullSessionID = new DeviceLinkSessionResponse(); - responseWithNullSessionID.setSessionToken("test-session-token"); - responseWithNullSessionID.setSessionSecret("test-session-secret"); - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Dynamic link certificate choice session failed: invalid response received.", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_userAccountNotFound() { - when(connector.initDynamicLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); - - var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); - assertEquals(UserAccountNotFoundException.class, ex.getClass()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyUUID() { - builderService.withRelyingPartyUUID(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyName() { - builderService.withRelyingPartyName(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_invalidNonce() { - builderService.withNonce("1234567890123456789012345678901"); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_emptyNonce() { - builderService.withNonce(""); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); - } - } - - private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - return response; - } -} diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 1e4eec6c..88b7975a 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -74,22 +74,24 @@ void setUp() { @Nested @WireMockTest(httpPort = 18089) - class DynamicLinkCertificateChoiceSession { + class DeviceLinkCertificateChoiceSession { @Test - void createDynamicLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/dynamic-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); + void createDeviceLinkCertificateChoice() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); - DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) + .withInitialCallbackUrl("https://smart-id.com/device-link/") .initCertificateChoice(); assertNotNull(response.getSessionID()); assertNotNull(response.getSessionToken()); assertNotNull(response.getSessionSecret()); + assertNotNull(response.getDeviceLinkBase()); assertNotNull(response.getReceivedAt()); } } @@ -184,7 +186,7 @@ void createDeviceLinkSignature_withDocumentNumber() { .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackURL("https://example.com/callback") + .withInitialCallbackUrl("https://example.com/callback") .initSignatureSession(); assertNotNull(response.getSessionID()); @@ -207,7 +209,7 @@ void createDeviceLinkSignature_withSemanticsIdentifier() { .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackURL("https://example.com/callback") + .withInitialCallbackUrl("https://example.com/callback") .initSignatureSession(); assertNotNull(response.getSessionID()); @@ -421,13 +423,11 @@ void createDynamicContent_authenticationWithQRCode() { assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.getSessionToken()); } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") - @ParameterizedTest - @EnumSource - void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); + @Test + void createDynamicContent_certificateChoiceWithWithQRCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); - DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); @@ -436,7 +436,7 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceL URI fullUri = new DeviceLinkBuilder() .withDeviceLinkBase(response.getDeviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(response.getSessionToken()) .withElapsedSeconds(elapsedSeconds) @@ -444,15 +444,37 @@ void createDynamicContent_certificateChoiceWithDifferentDynamicLinkTypes(DeviceL .withRelyingPartyName("DEMO") .buildDeviceLink(response.getSessionSecret()); + assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.getSessionToken()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = { "WEB_2_APP", "APP_2_APP" }) + void createDynamicContent_certificateChoiceWithWithWeb2AppAndApp2App(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + URI fullUri = new DeviceLinkBuilder() + .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.getSessionToken()) + .withLang("eng") + .withRelyingPartyName("DEMO") + .withInitialCallbackUrl("https://smart-id.com/callback") + .buildDeviceLink(response.getSessionSecret()); + assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/dynamic-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); - DeviceLinkSessionResponse response = smartIdClient.createDynamicLinkCertificateRequest() + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 8e5ffd34..3c428d02 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -120,7 +120,7 @@ void anonymousAuthentication_withApp2App() { .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRpChallenge(rpChallenge) - .withInitialCallbackURL("https://example.com/callback") + .withInitialCallbackUrl("https://example.com/callback") .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPIN("Log in?") )); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index f4ebac93..5aa3833a 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -144,21 +144,21 @@ private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionReq } } - @Disabled("Endpoint not yet available") @Nested class CertificateChoice { @Test - void initDynamicLinkCertificateChoice() { + void initDeviceLinkCertificateChoice() { var request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID(RELYING_PARTY_UUID); request.setRelyingPartyName(RELYING_PARTY_NAME); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDynamicLinkCertificateChoice(request); + DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); + assertNotNull(sessionsResponse.getDeviceLinkBase()); assertNotNull(sessionsResponse.getReceivedAt()); } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 73089982..07923acd 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -518,12 +518,11 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-101") @Nested @WireMockTest(httpPort = 18089) - class DynamicLinkCertificateChoiceTests { + class DeviceLinkCertificateChoiceTests { - private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/certificatechoice/dynamic-link/anonymous"; + private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/certificatechoice/device-link/anonymous"; private SmartIdConnector connector; @@ -533,101 +532,101 @@ public void setUp() { } @Test - void initDynamicLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/dynamic-link-certificate-choice-session-response.json"); + void initDeviceLinkCertificateChoice() { + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/device-link-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDynamicLinkCertificateChoice(request); + DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); Instant end = Instant.now(); assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); } @Test - void initDynamicLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { + void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); request.setCertificateLevel("INVALID_LEVEL"); stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_userAccountNotFound() { + void initDeviceLinkCertificateChoice_userAccountNotFound() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_relyingPartyNoPermission() { + void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_invalidRequest() { + void initDeviceLinkCertificateChoice_invalidRequest() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - CertificateChoiceSessionRequest request = new CertificateChoiceSessionRequest(); + var request = new CertificateChoiceSessionRequest(); request.setRelyingPartyUUID(""); request.setRelyingPartyName(""); - assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Request is unauthorized for URI http://localhost:18089/certificatechoice/dynamic-link/anonymous", exception.getMessage()); + assertEquals("Request is unauthorized for URI http://localhost:18089/certificatechoice/device-link/anonymous", exception.getMessage()); } @Test - void initDynamicLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { + void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { + void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @Test - void initDynamicLinkCertificateChoice_throwsSmartIdClientException() { + void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @Test - void initDynamicLinkCertificateChoice_throwsServerMaintenanceException() { + void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(ServerMaintenanceException.class, () -> connector.initDynamicLinkCertificateChoice(request)); + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } } diff --git a/src/test/resources/responses/dynamic-link-certificate-choice-session-response.json b/src/test/resources/responses/device-link-certificate-choice-session-response.json similarity index 81% rename from src/test/resources/responses/dynamic-link-certificate-choice-session-response.json rename to src/test/resources/responses/device-link-certificate-choice-session-response.json index 30724345..3e18ae14 100644 --- a/src/test/resources/responses/dynamic-link-certificate-choice-session-response.json +++ b/src/test/resources/responses/device-link-certificate-choice-session-response.json @@ -2,5 +2,6 @@ "sessionID": "00000000-0000-0000-0000-000000000000", "sessionToken": "sampleSessionToken", "sessionSecret": "sampleSessionSecret", + "deviceLinkBase": "https://smart-id.com/device-link/", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file From 9f59c8e4fc84cb9ec05f9ac1c63f16b7611c2f81 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 11 Aug 2025 11:14:11 +0300 Subject: [PATCH 30/57] fix #121; improve usage of enums (#123) --- .../DefaultAuthenticationResponseMapper.java | 11 +++-- src/main/java/ee/sk/smartid/FlowType.java | 16 +++++-- .../java/ee/sk/smartid/MaskGenAlgorithm.java | 8 ++-- .../ee/sk/smartid/SignatureAlgorithm.java | 11 +++-- .../smartid/SignatureResponseValidator.java | 36 +++++++-------- src/main/java/ee/sk/smartid/TrailerField.java | 8 ++-- ...faultAuthenticationResponseMapperTest.java | 45 +++++++++++++++++++ 7 files changed, 93 insertions(+), 42 deletions(-) diff --git a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java index ca486453..65f6b1f2 100644 --- a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java @@ -80,21 +80,21 @@ public AuthenticationResponse from(SessionStatus sessionStatus) { authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); - authenticationResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); + authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()).orElse(null); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); authenticationResponse.setSignatureAlgorithm(signatureAlgorithm); var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); var hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null); authenticationResponse.setHashAlgorithm(hashAlgorithm); - MaskGenAlgorithm maskGenAlgorithm = MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm()).orElse(null); + MaskGenAlgorithm maskGenAlgorithm = MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm()); authenticationResponse.setMaskGenAlgorithm(maskGenAlgorithm); var maskGenHashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null); authenticationResponse.setMaskHashAlgorithm(maskGenHashAlgorithm); authenticationResponse.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - TrailerField trailerField = TrailerField.fromString(signatureAlgorithmParameters.getTrailerField()).orElse(null); + TrailerField trailerField = TrailerField.fromString(signatureAlgorithmParameters.getTrailerField()); authenticationResponse.setTrailerField(trailerField); authenticationResponse.setCertificate(toCertificate(sessionCertificate)); @@ -193,8 +193,7 @@ private static void validateSignature(SessionSignature sessionSignature) { if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithm` is empty"); } - Optional signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); - if (signatureAlgorithm.isEmpty()) { + if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { logger.error("Invalid `signature.signatureAlgorithm` in the session status: {}", sessionSignature.getSignatureAlgorithm()); throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithm` in the session status"); } diff --git a/src/main/java/ee/sk/smartid/FlowType.java b/src/main/java/ee/sk/smartid/FlowType.java index c487e6a6..b79def14 100644 --- a/src/main/java/ee/sk/smartid/FlowType.java +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -45,7 +45,15 @@ public String getDescription() { return description; } - public static boolean isSupported(String input) { - return Arrays.stream(FlowType.values()).anyMatch(flowType -> flowType.getDescription().equals(input)); + public static boolean isSupported(String flowType) { + return Arrays.stream(FlowType.values()) + .anyMatch(f -> f.getDescription().equals(flowType)); + } + + public static FlowType fromString(String flowType) { + return Arrays.stream(FlowType.values()) + .filter(f -> f.getDescription().equals(flowType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid flowType value: " + flowType)); } } diff --git a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java index 4be8732d..63b8fc93 100644 --- a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -27,7 +27,6 @@ */ import java.util.Arrays; -import java.util.Optional; /** * Represents mask algorithm in the response and the value used in recrating the signature. @@ -52,9 +51,10 @@ public String getMgfName() { return mgfName; } - public static Optional fromString(String input) { + public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { return Arrays.stream(MaskGenAlgorithm.values()) - .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) - .findFirst(); + .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid maskGenAlgorithm value: " + maskGenAlgorithm)); } } diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index ca894220..6d4cc9e5 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -27,7 +27,6 @@ */ import java.util.Arrays; -import java.util.Optional; public enum SignatureAlgorithm { @@ -43,10 +42,16 @@ public String getAlgorithmName() { return algorithmName; } - public static Optional fromString(String signatureAlgorithm) { + public static boolean isSupported(String signatureAlgorithm) { + return Arrays.stream(SignatureAlgorithm.values()) + .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); + } + + public static SignatureAlgorithm fromString(String signatureAlgorithm) { return Arrays .stream(SignatureAlgorithm.values()) .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) - .findFirst(); + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid signatureAlgorithm value: " + signatureAlgorithm)); } } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index bd783499..6d318edc 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -114,7 +114,7 @@ public SignatureResponse from(SessionStatus sessionStatus, SessionSignature sessionSignature = sessionStatus.getSignature(); SessionCertificate certificate = sessionStatus.getCert(); - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()).orElse(null); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); HashAlgorithm hashAlgorithm = HashAlgorithm.fromString(sessionSignature.getSignatureAlgorithmParameters().getHashAlgorithm()).orElse(null); SessionMaskGenAlgorithm maskGenAlgorithm = sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm(); HashAlgorithm maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()).orElse(null); @@ -271,7 +271,9 @@ private static boolean isCertificateLevelValid(String requestedCertificateLevel, private static Set getPolicyOids(X509Certificate certificate) { Set result = new HashSet<>(); byte[] extensionValue = certificate.getExtensionValue("2.5.29.32"); - if (extensionValue == null) return result; + if (extensionValue == null) { + return result; + } try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { @@ -288,14 +290,18 @@ private static Set getPolicyOids(X509Certificate certificate) { private static boolean containsQcStatement(X509Certificate cert) { byte[] extensionValue = cert.getExtensionValue("1.3.6.1.5.5.7.1.3"); - if (extensionValue == null) return false; + if (extensionValue == null) { + return false; + } try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { ASN1Sequence seq = (ASN1Sequence) ais2.readObject(); for (int i = 0; i < seq.size(); i++) { QCStatement st = QCStatement.getInstance(seq.getObjectAt(i)); - if (QC_STATEMENT_OID.equals(st.getStatementId().getId())) return true; + if (QC_STATEMENT_OID.equals(st.getStatementId().getId())) { + return true; + } } } } catch (Exception ex) { @@ -323,7 +329,6 @@ private static void validateRawDigestSignature(SessionStatus sessionStatus) { validateSignatureValue(signature.getValue()); validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); validateFlowType(signature.getFlowType()); - validateSignatureAlgorithm(signature.getSignatureAlgorithm()); validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); @@ -335,18 +340,14 @@ private static void validateSignatureValue(String value) { } } - private static void validateSignatureAlgorithmName(String algorithm) { - if (StringUtil.isEmpty(algorithm)) { + private static void validateSignatureAlgorithmName(String signatureAlgorithm) { + if (StringUtil.isEmpty(signatureAlgorithm)) { throw new UnprocessableSmartIdResponseException("Signature algorithm is missing"); } - List allowedSignatureAlgorithms = Arrays.stream(SignatureAlgorithm.values()) - .map(SignatureAlgorithm::getAlgorithmName) - .toList(); - - if (!allowedSignatureAlgorithms.contains(algorithm)) { - throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + allowedSignatureAlgorithms + ", but got: " + algorithm - ); + if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { + List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); + throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + possibleValues + ", but got: " + signatureAlgorithm); } } @@ -360,13 +361,6 @@ private static void validateFlowType(String flowType) { } } - private static void validateSignatureAlgorithm(String algorithm) { - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(algorithm).orElse(null); - if (signatureAlgorithm != SignatureAlgorithm.RSASSA_PSS) { - throw new UnprocessableSmartIdResponseException("signatureAlgorithm must be rsassa-pss"); - } - } - private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { if (sessionSignatureAlgorithmParameters == null) { throw new UnprocessableSmartIdResponseException("SignatureAlgorithmParameters is missing"); diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index 41901325..b7302790 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -27,7 +27,6 @@ */ import java.util.Arrays; -import java.util.Optional; /** * TrailerField represents the value used in the trailer field of the Smart-ID authentication response and value necessary for generating the signature. @@ -48,13 +47,14 @@ public String getValue() { return value; } - public int getPssSpecValue(){ + public int getPssSpecValue() { return pssSpecValue; } - public static Optional fromString(String trailerField) { + public static TrailerField fromString(String trailerField) { return Arrays.stream(TrailerField.values()) .filter(field -> field.getValue().equals(trailerField)) - .findFirst(); + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid trailerField value: " + trailerField)); } } diff --git a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java index 91833a73..3a5a0853 100644 --- a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; @@ -84,6 +85,50 @@ void from() { assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); } + @ParameterizedTest + @EnumSource(FlowType.class) + void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.setFlowType(flowType.getDescription()); + var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); + var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + assertEquals(hashAlgorithm, authenticationResponse.getHashAlgorithm()); + assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getSaltLength()); + } + @Test void from_sessionStatusNull_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); From 985bfbc2e6bc42ef9c9f1f5fdf900427120c2ed1 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 18 Aug 2025 11:14:51 +0300 Subject: [PATCH 31/57] Improve authentication session request and session status exception messages (#124) * SLIB-95 - improve device link authentication session request validation exception messages * SLIB-95 - improve authentication response validation exception messages --- CHANGELOG.md | 3 + README.md | 4 +- .../DefaultAuthenticationResponseMapper.java | 101 ++-- ...nkAuthenticationSessionRequestBuilder.java | 63 +-- .../SmartIdRequestSetupException.java | 43 ++ .../AuthenticationResponseValidatorTest.java | 2 +- ...faultAuthenticationResponseMapperTest.java | 440 +++++++----------- ...thenticationSessionRequestBuilderTest.java | 65 +-- 8 files changed, 333 insertions(+), 388 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 959d798a..2c24ba8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.8] - 2025-07-15 +- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. + ## [3.1.7] - 2025-07-10 - Renamed dynamic-link certificate choice to device-link certificate choice. diff --git a/README.md b/README.md index f77136ce..c5aad4b0 100644 --- a/README.md +++ b/README.md @@ -1236,7 +1236,9 @@ Exception Categories These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input * `SmartIdClientException` Thrown for general client-side errors, such as: * Missing or invalid configuration (e.g., `trustSslContext` not set). - * Unexpected response data (e.g., missing required fields in session status.) + * `SmartIdRequestSetupException` Thrown when the request field validations fails, such as: + * Missing required fields (e.g., `relyingPartyUUID`, `relyingPartyName`, `signatureProtocol`). + * Invalid values for fields (e.g. `interactionType` containing duplicate types). * Unprocessable Response Exceptions These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. diff --git a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java index 65f6b1f2..573a2664 100644 --- a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java @@ -108,7 +108,7 @@ public AuthenticationResponse from(SessionStatus sessionStatus) { private static void validateSessionStatus(SessionStatus sessionStatus) { if (sessionStatus == null) { - throw new SmartIdClientException("Input parameter `sessionsStatus` is not provided"); + throw new SmartIdClientException("Parameter 'sessionsStatus' is not provided"); } validateResult(sessionStatus.getResult()); @@ -117,85 +117,85 @@ private static void validateSessionStatus(SessionStatus sessionStatus) { validateCertificate(sessionStatus.getCert()); if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("Session status field `interactionTypeUsed` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'interactionTypeUsed' is empty"); } } private static void validateResult(SessionResult sessionResult) { if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Session status field `result` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result' is empty"); } String endResult = sessionResult.getEndResult(); if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Session status field `result.endResult` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.endResult' is empty"); } if (!"OK".equals(endResult)) { ErrorResultHandler.handle(sessionResult); } if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Session status field `result.documentNumber` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.documentNumber' is empty"); } } private static void validateSignatureProtocol(SessionStatus sessionStatus) { if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Session status field `signatureProtocol` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' is empty"); } if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { - logger.error("Invalid `signatureProtocol` in authentication sessions status: {}", sessionStatus.getSignatureProtocol()); - throw new UnprocessableSmartIdResponseException("Invalid `signatureProtocol` in sessions status"); + logger.error("Authentication session status field 'signatureProtocol' has invalid value: {}", sessionStatus.getSignatureProtocol()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' has unsupported value"); } } private static void validateSignature(SessionSignature sessionSignature) { if (sessionSignature == null) { - throw new UnprocessableSmartIdResponseException("Session status field `signature` is missing"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature' is missing"); } if (StringUtil.isEmpty(sessionSignature.getValue())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.value` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' is empty"); } if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { - logger.error("Session status field `signature.value` is not in Base64-encoded format: {}", sessionSignature.getValue()); - throw new UnprocessableSmartIdResponseException("Session status field `signature.value` is not in Base64-encoded format"); + logger.error("Authentication session status field 'signature.value' does not have Base64-encoded value: {}", sessionSignature.getValue()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' does not have Base64-encoded value"); } if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.severRandom` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' is empty"); } int serverRandomLength = sessionSignature.getServerRandom().length(); if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { - logger.error("Signature field `serverRandom` is less than required length: expected {} < {}", serverRandomLength, MINIMUM_SERVER_RANDOM_LENGTH); - throw new UnprocessableSmartIdResponseException("Session status field `signature.serverRandom` is less than required length"); + logger.error("Authentication session status field 'signature.serverRandom' is less than required length. Expected: {}; Actual: {}", MINIMUM_SERVER_RANDOM_LENGTH, serverRandomLength); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' value length is less than required"); } if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { - logger.error("Session status field `signature.serverRandom` is not in Base64-encoded format: {}", sessionSignature.getServerRandom()); - throw new UnprocessableSmartIdResponseException("Session status field `signature.serverRandom` is not in Base64-encoded format"); + logger.error("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value: {}", sessionSignature.getServerRandom()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value"); } if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.userChallenge` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' is empty"); } if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { - logger.error("`signature.userChallenge` value in session status is not in the expected Base64-encoded format: {}", sessionSignature.getUserChallenge()); - throw new UnprocessableSmartIdResponseException("`signature.userChallenge` value in session status is not in the expected Base64-encoded format"); + logger.error("Authentication session status field 'signature.userChallenge' does not match required pattern. Expected pattern {}; actual value {}", USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' value does not match required pattern"); } if (StringUtil.isEmpty(sessionSignature.getFlowType())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.flowType` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' is empty"); } if (!FlowType.isSupported(sessionSignature.getFlowType())) { - logger.error("Invalid `signature.flowType` in session status: {}", sessionSignature.getFlowType()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.flowType` in session status"); + logger.error("Authentication session status field 'signature.flowType' has invalid value: {}", sessionSignature.getFlowType()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' has unsupported value"); } if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithm` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' is empty"); } if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { - logger.error("Invalid `signature.signatureAlgorithm` in the session status: {}", sessionSignature.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithm` in the session status"); + logger.error("Authentication session status field 'signature.signatureAlgorithm' has invalid value: {}", sessionSignature.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' has unsupported value"); } validateSignatureAlgorithmParameters(sessionSignature); @@ -204,74 +204,79 @@ private static void validateSignature(SessionSignature sessionSignature) { private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); if (sessionSignature.getSignatureAlgorithmParameters() == null) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters` is missing"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); } if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.hashAlgorithm` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); } Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); if (hashAlgorithm.isEmpty()) { - logger.error("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status: {}", signatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status"); + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", signatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); } var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm` is missing"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); } if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); } if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status"); + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value"); } + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); + } Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); if (maskGenHashAlgorithm.isEmpty()) { - logger.error("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status: {}", - maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in empty"); + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); } if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { - logger.error("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`: expected {}, got {}", + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", hashAlgorithm.get().getAlgorithmName(), maskGenHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); } if (signatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.saltLength` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty"); } int octetLength = hashAlgorithm.get().getOctetLength(); if (octetLength != signatureAlgorithmParameters.getSaltLength()) { - logger.error("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status: expected {}, got {}", + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: {}, actual: {}", octetLength, signatureAlgorithmParameters.getSaltLength()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); } if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); } if (!TrailerField.OXBC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { - logger.error("Invalid `signature.signatureAlgorithmParameters.trailerField` in session status: {}", signatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status"); + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); } } private static void validateCertificate(SessionCertificate sessionCertificate) { if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert' is missing"); } if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.value' is empty"); } if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); } } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index 9bf2a9aa..f4104ffc 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -30,11 +30,9 @@ import java.util.List; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; @@ -53,7 +51,6 @@ */ public class DeviceLinkAuthenticationSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(DeviceLinkAuthenticationSessionRequestBuilder.class); private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; private final SmartIdConnector connector; @@ -241,7 +238,7 @@ public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(Stri * * * @return init session response - * @throws SmartIdClientException if request parameters are invalid + * @throws SmartIdRequestSetupException if the provided values for the request are invalid * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initAuthenticationSession() { @@ -268,8 +265,7 @@ public AuthenticationSessionRequest getAuthenticationSessionRequest() { private DeviceLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null && documentNumber != null) { - logger.error("Both semanticsIdentifier and documentNumber are set – only one can be used"); - throw new SmartIdClientException("Only one of semanticsIdentifier or documentNumber may be set"); + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); } if (semanticsIdentifier != null) { return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); @@ -282,12 +278,10 @@ private DeviceLinkSessionResponse initAuthenticationSession(AuthenticationSessio private void validateRequestParameters() { if (StringUtil.isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } validateSignatureParameters(); validateInteractions(); @@ -296,41 +290,41 @@ private void validateRequestParameters() { private void validateSignatureParameters() { if (StringUtil.isEmpty(rpChallenge)) { - logger.error("Parameter rpChallenge must be set"); - throw new SmartIdClientException("Parameter rpChallenge must be set"); + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); } try { Base64.getDecoder().decode(rpChallenge); } catch (IllegalArgumentException e) { - logger.error("Parameter rpChallenge is not a valid Base64 encoded string"); - throw new SmartIdClientException("Parameter rpChallenge is not a valid Base64 encoded string"); + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); } if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { - logger.error("Encoded rpChallenge must be between 44 and 88 characters"); - throw new SmartIdClientException("Encoded rpChallenge must be between 44 and 88 characters"); + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); } if (signatureAlgorithm == null) { - logger.error("Parameter signatureAlgorithm must be set"); - throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); } if (hashAlgorithm == null) { - logger.error("Parameter hashAlgorithm must be set"); - throw new SmartIdClientException("Parameter hashAlgorithm must be set"); + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); } } private void validateInteractions() { if (interactions == null || interactions.isEmpty()) { - logger.error("Parameter interactions must be set"); - throw new SmartIdClientException("Parameter interactions must be set"); + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } validateNoDuplicateInteractions(); interactions.forEach(DeviceLinkInteraction::validate); } + private void validateNoDuplicateInteractions() { + if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + private void validateInitialCallbackUrl() { if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } @@ -354,29 +348,20 @@ private AuthenticationSessionRequest createAuthenticationRequest() { private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionToken())) { - logger.error("Session token is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionSecret())) { - logger.error("Session secret is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); - } - if (deviceLinkAuthenticationSessionResponse.getDeviceLinkBase() == null || deviceLinkAuthenticationSessionResponse.getDeviceLinkBase().toString().isBlank()) { - logger.error("deviceLinkBase is missing or empty in the response"); - throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing or empty in the response"); + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); } - } + if (deviceLinkAuthenticationSessionResponse.getDeviceLinkBase() == null + || deviceLinkAuthenticationSessionResponse.getDeviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); - private void validateNoDuplicateInteractions() { - if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { - logger.error("Duplicate values found in interactions"); - throw new SmartIdClientException("Duplicate values in interactions are not allowed"); } } } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java new file mode 100644 index 00000000..4088fa86 --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java @@ -0,0 +1,43 @@ +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exception thrown when there is an issue setting up a Smart-ID request. + * This could be due to invalid parameters, configuration issues, or other + * problems that prevent from successfully preparing the request. + */ +public class SmartIdRequestSetupException extends SmartIdClientException { + + public SmartIdRequestSetupException(String message) { + super(message); + } + + public SmartIdRequestSetupException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 367569c4..e7f8926f 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -136,7 +136,7 @@ void validate_emptySchemaNameIsProvided_throwException(String schemaName) { @Test void validate_sessionStatusResultIsNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("Session status field `result` is empty", ex.getMessage()); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); } @Nested diff --git a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java index 3a5a0853..5401c49a 100644 --- a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java @@ -132,7 +132,7 @@ void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorit @Test void from_sessionStatusNull_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); - assertEquals("Input parameter `sessionsStatus` is not provided", exception.getMessage()); + assertEquals("Parameter 'sessionsStatus' is not provided", exception.getMessage()); } @Nested @@ -142,7 +142,7 @@ class ValidateResult { void from_sessionResultIsNotPresent_throwException() { var sessionStatus = new SessionStatus(); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `result` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'result' is empty", exception.getMessage()); } @ParameterizedTest @@ -155,7 +155,7 @@ void from_endResultIsNotPresent_throwException(String endResult) { sessionStatus.setResult(sessionResult); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `result.endResult` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'result.endResult' is empty", exception.getMessage()); } @ParameterizedTest @@ -195,7 +195,7 @@ void from_documentNumberIsEmpty_throwException(String documentNumber) { sessionStatus.setResult(sessionResult); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `result.documentNumber` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'result.documentNumber' is empty", exception.getMessage()); } } @@ -210,7 +210,7 @@ void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol sessionStatus.setSignatureProtocol(signatureProtocol); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signatureProtocol` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signatureProtocol' is empty", exception.getMessage()); } @ParameterizedTest @@ -223,7 +223,7 @@ void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignat sessionStatus.setSignatureProtocol(invalidSignatureProtocol); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signatureProtocol` in sessions status", exception.getMessage()); + assertEquals("Authentication session status field 'signatureProtocol' has unsupported value", exception.getMessage()); } @Nested @@ -238,7 +238,7 @@ void from_signatureIsNotProvided_throwException() { sessionStatus.setSignatureProtocol("ACSP_V2"); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature` is missing", exception.getMessage()); + assertEquals("Authentication session status field 'signature' is missing", exception.getMessage()); } @ParameterizedTest @@ -249,13 +249,10 @@ void from_signatureValueIsNotProvided_throwException(String signatureValue) { var sessionSignature = new SessionSignature(); sessionSignature.setValue(signatureValue); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.value` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.value' is empty", exception.getMessage()); } @ParameterizedTest @@ -266,13 +263,10 @@ void from_signatureValueDoesNotMatchThePattern_throwException(String signatureVa var sessionSignature = new SessionSignature(); sessionSignature.setValue(signatureValue); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.value` is not in Base64-encoded format", exception.getMessage()); + assertEquals("Authentication session status field 'signature.value' does not have Base64-encoded value", exception.getMessage()); } @ParameterizedTest @@ -284,13 +278,10 @@ void from_serverRandomIsNotProvided_throwException(String serverRandom) { sessionSignature.setValue("signatureValue"); sessionSignature.setServerRandom(serverRandom); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.severRandom` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.serverRandom' is empty", exception.getMessage()); } @Test @@ -301,13 +292,10 @@ void from_serverRandomLengthIsLessThanAllowed_throwException() { sessionSignature.setValue("signatureValue"); sessionSignature.setServerRandom("a".repeat(23)); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.serverRandom` is less than required length", exception.getMessage()); + assertEquals("Authentication session status field 'signature.serverRandom' value length is less than required", exception.getMessage()); } @ParameterizedTest @@ -319,13 +307,10 @@ void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRa sessionSignature.setValue("signatureValue"); sessionSignature.setServerRandom(serverRandom); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.serverRandom` is not in Base64-encoded format", exception.getMessage()); + assertEquals("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value", exception.getMessage()); } @ParameterizedTest @@ -338,13 +323,10 @@ void from_userChallengeIsEmpty_throwException(String userChallenge) { sessionSignature.setServerRandom("a".repeat(24)); sessionSignature.setUserChallenge(userChallenge); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.userChallenge` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.userChallenge' is empty", exception.getMessage()); } @ParameterizedTest @@ -357,13 +339,10 @@ void from_providedUserChallengeDoesNotMatchThePattern_throwException(String user sessionSignature.setServerRandom("a".repeat(24)); sessionSignature.setUserChallenge(userChallenge); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("`signature.userChallenge` value in session status is not in the expected Base64-encoded format", exception.getMessage()); + assertEquals("Authentication session status field 'signature.userChallenge' value does not match required pattern", exception.getMessage()); } @ParameterizedTest @@ -377,13 +356,10 @@ void from_flowTypeNotProvided_throwException(String flowType) { sessionSignature.setUserChallenge("a".repeat(43)); sessionSignature.setFlowType(flowType); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.flowType` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.flowType' is empty", exception.getMessage()); } @Test @@ -396,29 +372,22 @@ void from_flowTypeNotSupported_throwException() { sessionSignature.setUserChallenge("a".repeat(43)); sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.flowType` in session status", exception.getMessage()); + assertEquals("Authentication session status field 'signature.flowType' has unsupported value", exception.getMessage()); } - @ParameterizedTest @NullAndEmptySource void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); var sessionSignature = toSessionSignature(signatureAlgorithm); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithm` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' is empty", exception.getMessage()); } @Test @@ -432,13 +401,10 @@ void from_signatureAlgorithmIsNotSupported_throwException() { sessionSignature.setFlowType("QR"); sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.signatureAlgorithm` in the session status", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' has unsupported value", exception.getMessage()); } @Nested @@ -447,21 +413,11 @@ class ValidateSignatureAlgorithmParameters { @Test void from_signatureAlgorithmParametersAreMissing_throwException() { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionSignature = toSessionSignature(null); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters` is missing", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters' is missing", exception.getMessage()); } @ParameterizedTest @@ -469,315 +425,265 @@ void from_signatureAlgorithmParametersAreMissing_throwException() { void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters.hashAlgorithm` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", exception.getMessage()); } - @Test - void from_hashAlgorithmIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-1"); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + signatureAlgorithmParameters.setHashAlgorithm(invalidHashAlgorithm); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.signatureAlgorithmParameters.hashAlgorithm` in session status", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); } @Test void from_masGenAlgorithmIsMissing_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm` is missing", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(algorithm); var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(algorithm); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", exception.getMessage()); } @Test void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("invalid"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_parametersInMaskGenAlgorithmAreMissing_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(null); var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("invalid"); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.signatureAlgorithmParameters.maskGenAlgorithm` in session status", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource - void from_hashAlgorithmInMaskGenAlgorithmIsEmpty_throwException(String hashAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + void from_hashAlgorithmInMaskGenAlgorithmParametersIsEmpty_throwException(String hashAlgorithm) { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", exception.getMessage()); + } + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "asdhfasdf"}) + void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String hashAlgorithm) { var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); } @Test void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); var maskGenAlgorithm = new SessionMaskGenAlgorithm(); maskGenAlgorithm.setAlgorithm("id-mgf1"); - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("`signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm` in session status does not match `signature.signatureAlgorithmParameters.hashAlgorithm`", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); } @Test void from_saltLengthIsMissing_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(null); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); maskGenAlgorithm.setAlgorithm("id-mgf1"); - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - signatureAlgorithmParameters.setSaltLength(null); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.saltLength` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty", exception.getMessage()); } @Test void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(20); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); maskGenAlgorithm.setAlgorithm("id-mgf1"); - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - signatureAlgorithmParameters.setSaltLength(20); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.signatureAlgorithmParameters.saltLength` in session status", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); } - @Test - void from_trailerFieldIsEmpty_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - + @ParameterizedTest + @NullAndEmptySource + void from_trailerFieldIsEmpty_throwException(String trailerField) { var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField(trailerField); + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); maskGenAlgorithm.setAlgorithm("id-mgf1"); - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - signatureAlgorithmParameters.setSaltLength(64); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty", exception.getMessage()); } @Test void from_trailerFieldValueIsInvalid_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("invalid"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); + } + private static SessionSignature toSessionSignature(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { var sessionSignature = new SessionSignature(); sessionSignature.setValue("signatureValue"); sessionSignature.setServerRandom("a".repeat(24)); sessionSignature.setUserChallenge("a".repeat(43)); sessionSignature.setFlowType("QR"); sessionSignature.setSignatureAlgorithm("rsassa-pss"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField("invalid"); sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + } - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + return sessionStatus; + } - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status", exception.getMessage()); - } + private static SessionMaskGenAlgorithmParameters toMaskGenAlgorithmParameters() { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + return maskGenAlgorithmParameters; } } @@ -795,7 +701,7 @@ void from_sessionCertificateIsNotProvided_throwException() { sessionStatus.setSignature(sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Certificate parameter is missing in session status", exception.getMessage()); + assertEquals("Authentication session status field 'cert' is missing", exception.getMessage()); } @ParameterizedTest @@ -814,7 +720,7 @@ void from_certificateValueIsNotProvided_throwException(String certificateValue) sessionStatus.setCert(sessionCertificate); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Value parameter is missing in certificate", exception.getMessage()); + assertEquals("Authentication session status field 'cert.value' is empty", exception.getMessage()); } @ParameterizedTest @@ -831,7 +737,7 @@ void from_certificateLevelIsNotProvided_throwException(String certificateLevel) sessionStatus.setCert(sessionCertificate); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Certificate level parameter is missing in certificate", exception.getMessage()); + assertEquals("Authentication session status field 'cert.certificateLevel' is empty", exception.getMessage()); } @Test @@ -867,7 +773,7 @@ void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUs sessionStatus.setInteractionTypeUsed(interactionFlowUsed); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Session status field `interactionTypeUsed` is empty", exception.getMessage()); + assertEquals("Authentication session status field 'interactionTypeUsed' is empty", exception.getMessage()); } private static SessionResult toSessionResult(String documentNumber) { diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 59924868..9d967382 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -62,12 +62,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.HashAlgorithm; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; class DeviceLinkAuthenticationSessionRequestBuilderTest { @@ -246,44 +247,44 @@ void initAuthenticationSession_initialCallbackUrlIsValid_ok() { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID(relyingPartyUUID) .withRelyingPartyName("DEMO") .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession()); - assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName(relyingPartyName) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession()); - assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession()); - assertEquals("Parameter rpChallenge must be set", exception.getMessage()); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -296,7 +297,7 @@ void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChal @Test void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -304,13 +305,13 @@ void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { .withSignatureAlgorithm(null) .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession()); - assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -318,13 +319,13 @@ void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(Li .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(interactions) .initAuthenticationSession()); - assertEquals("Parameter interactions must be set", exception.getMessage()); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); } @ParameterizedTest @ArgumentsSource(DuplicateInteractionsProvider.class) void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -333,7 +334,7 @@ void initAuthenticationSession_duplicateInteractions_throwException(List + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -368,7 +369,7 @@ void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String @Test void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -377,12 +378,12 @@ void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) .initAuthenticationSession() ); - assertEquals("Parameter hashAlgorithm must be set", exception.getMessage()); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); } @Test void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -392,12 +393,12 @@ void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_t .initAuthenticationSession() ); - assertEquals("Parameter hashAlgorithm must be set", exception.getMessage()); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); } @Test void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -409,7 +410,7 @@ void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throw .initAuthenticationSession() ); - assertEquals("Only one of semanticsIdentifier or documentNumber may be set", exception.getMessage()); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); } private DeviceLinkInteraction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { @@ -433,7 +434,7 @@ void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException initAuthentication(); }); - assertEquals("Session ID is missing from the response", exception.getMessage()); + assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); } @ParameterizedTest @@ -447,7 +448,7 @@ void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwExcept initAuthentication(); }); - assertEquals("Session token is missing from the response", exception.getMessage()); + assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); } @ParameterizedTest @@ -462,7 +463,7 @@ void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwExcep initAuthentication(); }); - assertEquals("Session secret is missing from the response", exception.getMessage()); + assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); } @ParameterizedTest @@ -479,7 +480,7 @@ void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(Str initAuthentication(); }); - assertEquals("deviceLinkBase is missing or empty in the response", exception.getMessage()); + assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); } private void initAuthentication() { @@ -575,11 +576,11 @@ private static class InvalidRpChallengeArgumentProvider implements ArgumentsProv public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Parameter rpChallenge is not a valid Base64 encoded string"), + "Value for 'rpChallenge' must be Base64-encoded string"), Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(30).getBytes())), - "Encoded rpChallenge must be between 44 and 88 characters"), + "Value for 'rpChallenge' must have length between 44 and 88 characters"), Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(67).getBytes())), - "Encoded rpChallenge must be between 44 and 88 characters") + "Value for 'rpChallenge' must have length between 44 and 88 characters") ); } } @@ -617,9 +618,9 @@ private static class InvalidInitialCallbackUrlArgumentProvider implements Argume @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + Arguments.of("http://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") ); } } From 382239a5f2b5830767867ef003ba90e893265a39 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 27 Aug 2025 11:06:10 +0300 Subject: [PATCH 32/57] Improve signature sessions status validations (#125) * SLIB-106 - improve signature session status response validation exception messages * SLIB-106 - move common certificate validations to CertificateValidator; update Readme * SLIB-106 - update SignatureSessionRequest and related objects to record-type. * SLIB-106 - extract signature value validations to its own class * SLIB-106 - update readme and migration guide * SLIB-106 - improve function naming, javadoc descriptions and readme * SLIB-106 - fix authentication response validator tests * SLIB-106 - update exceptions messages in DeviceLinkSignatureSessionRequestBuilder * SLIB-106 - remove todos; fix typos; improve code quality * SLIB-106 - update license headers --- CHANGELOG.md | 3 + MIGRATION_GUIDE.md | 47 +- README.md | 114 +-- .../ee/sk/smartid/AuthenticationIdentity.java | 4 +- .../ee/sk/smartid/AuthenticationResponse.java | 55 +- ... => AuthenticationResponseMapperImpl.java} | 29 +- .../AuthenticationResponseValidator.java | 147 +--- .../ee/sk/smartid/CertificateValidator.java | 44 + .../sk/smartid/CertificateValidatorImpl.java | 98 +++ ...iceLinkSignatureSessionRequestBuilder.java | 82 +- ...icationSignatureSessionRequestBuilder.java | 45 +- .../ee/sk/smartid/RsaSsaPssParameters.java | 82 ++ .../java/ee/sk/smartid/SignatureResponse.java | 54 +- .../smartid/SignatureResponseValidator.java | 191 ++--- .../sk/smartid/SignatureValueValidator.java | 46 + .../smartid/SignatureValueValidatorImpl.java | 112 +++ .../RawDigestSignatureProtocolParameters.java | 34 +- .../rest/dao/SignatureSessionRequest.java | 115 +-- ...AuthenticationResponseMapperImplTest.java} | 8 +- .../AuthenticationResponseValidatorTest.java | 49 +- .../smartid/CertificateValidatorImplTest.java | 81 ++ ...inkSignatureSessionRequestBuilderTest.java | 68 +- ...ionSignatureSessionRequestBuilderTest.java | 44 +- .../SignatureResponseValidatorTest.java | 390 +++++---- .../SignatureValueValidatorImplTest.java | 121 +++ .../integration/ReadmeIntegrationTest.java | 804 ++++++++++-------- .../SmartIdRestIntegrationTest.java | 76 +- .../rest/SmartIdRestConnectorTest.java | 61 +- 28 files changed, 1675 insertions(+), 1329 deletions(-) rename src/main/java/ee/sk/smartid/{DefaultAuthenticationResponseMapper.java => AuthenticationResponseMapperImpl.java} (92%) create mode 100644 src/main/java/ee/sk/smartid/CertificateValidator.java create mode 100644 src/main/java/ee/sk/smartid/CertificateValidatorImpl.java create mode 100644 src/main/java/ee/sk/smartid/RsaSsaPssParameters.java create mode 100644 src/main/java/ee/sk/smartid/SignatureValueValidator.java create mode 100644 src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java rename src/test/java/ee/sk/smartid/{DefaultAuthenticationResponseMapperTest.java => AuthenticationResponseMapperImplTest.java} (99%) create mode 100644 src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java create mode 100644 src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c24ba8d..e036a53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [3.1.9] - 2025-07-20 +- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and `SignatureResponseValidator`. + ## [3.1.8] - 2025-07-15 - Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index fbe17296..d4b8231c 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -16,10 +16,10 @@ It is recommended to start using device-link authentication flows from Smart-ID 1. Create authentication hash 2. Generate verification code from authentication hash 3. Verification code can be shown to the user -4. Create builder and set values. [Checkout setting values for authentication](README.md#examples-of-performing-authentication) +4. Create builder and set values. 5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. 6. After session status is `COMPLETE` response will be checked in the build method. -7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. [Validating authentication response](README.md#validating-authentication-response) +7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. ### Moving to V3 authentication flow @@ -33,26 +33,43 @@ It is recommended to start using device-link authentication flows from Smart-ID ## Migrating signing -Before migrating please read through [session types documentation](https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/sessions.html). It provides information about what has to be considered for implementing signing flow. -In here will be focusing on [signing on same device with prior authentication session](https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.2/sessions.html#_signing_with_prior_authentication_2). +Signing migration will be focusing on moving to signature flow when device link authentication has been completed before. ### Overview of V2 signing flow 1. Set values for certificate choice builder and call build method. Should return certificate as a response. -2. Use queried certificate to create DataToSign object. Requires digidoc4j library. +2. Use queried certificate to create DataToSign object. Requires DigiDoc4j library. 3. Create SignableData from DataToSign. 4. Create verification code from SignableData -5. Create signature builder and set values. [Checkout setting values for signing](README.md#create-the-signature) +5. Create signature builder and set values. 6. Call build method (`sign()`) to create signing session and to start polling for session status. 7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. -### Moving to V3 signing flow +### Moving to V3 signing flow - with DigiDoc4j library -1. Replace certificate choice builder with`NotificationCertificateChoiceSessionRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createNotificationCertificateChoice()` for easier access. Call build method `.initCertificateChoice()` to start the certificate choice session. Checkout example [here](README.md#examples-of-initiating-a-notification-based-certificate-choice-session). -2. Poll for session status with `sessionStatusPoller::fetchFinalSessionState(sessionID)`. -3. If session status state is `COMPLETE` then check response with `CertificateChoiceResponseMapper` for errors and to validate required fields. `CertificateChoiceResponse` will be returned when everything is ok. -4. Replace V2 SignableData with `ee.sk.smartid.SignableData`. In V3 SignableData the code to generate verification code was removed other than should be same as before. NB! If you are using Digidoc4j `DataToSign` make sure hash type in signable data matches digest algorithm in DataToSign. -5. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -6. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. -7. Poll for session status until its complete. -8. Validate session response with `SignatureSessionResponseMapper` and validate required fields. `SignatureSessionResponse` will be returned when everything is ok. \ No newline at end of file +DigiDoc4j library does not currently support signing with signature algorithm RSASSA-PSS. Support will be added in the future. +There is a possible workaround to use DigiDoc4j library and DSS library together to create ASICS container and sign it with Smart-ID v3 API. +Steps below include examples how to set up DataToSign for signing with RSASSA-PSS and how validate returned signature value. + +#### Steps to migrate + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. Example for setting up DataToSign with DSS: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdDeviceLinkSignatureService.java#L181 +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value. Example for validating signature value: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdSignatureService.java#L65 + +### Moving to V3 signing flow without DigiDoc4j library + +NB! Without DigiDoc4j library integrator has to provide implementation for creating signed container. +Smart-id-java-client only provides means to validate that signature response has required fields and returned signature value is valid. + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session status response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value with `SignatureValueValidator` diff --git a/README.md b/README.md index c5aad4b0..ebea79fe 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This library supports Smart-ID API v3.1. -# Table of contents +## Table of contents * [Smart-ID Java client](#smart-id-java-client) * [Introduction](#introduction) @@ -49,11 +49,13 @@ This library supports Smart-ID API v3.1. * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) * [Validating sessions status response](#validating-session-status-response) - * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) - * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) - * [Example of validating the signature](#example-of-validating-the-signature-session-response) - * [Error handling for session status](#error-handling-for-session-status) + * [Setting up CertificateValidator](#set-up-certificatevalidator) + * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) + * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) + * [Example of validating the signature](#example-of-validating-the-signature-session-response) + * [Error handling for session status](#error-handling-for-session-status) * [Certificate by document number](#certificate-by-document-number) + * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) * [Notification-based flows](#notification-based-flows) * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-dynamic-link-flows) * [Notification-based authentication session](#notification-based-authentication-session) @@ -197,7 +199,7 @@ String rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response -// Setup builder +// Set up builder DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number @@ -206,7 +208,7 @@ DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient DeviceLinkInteraction.displayTextAndPIN("Log in?") )); -// Init authentication session +// Initiate authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get authentication session request used for starting the authentication session and use it later to validate sessions status response @@ -224,7 +226,7 @@ URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); // Next steps: -// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Generate QR-code or device link to be displayed to the user // - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. @@ -255,7 +257,7 @@ DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient DeviceLinkInteraction.displayTextAndPIN("Log in?") )); -// Init authentication session +// Initiate authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get authentication session request used for starting the authentication session and use it later to validate sessions status response @@ -273,7 +275,7 @@ URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); // Next steps: -// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Generate QR-code or device link to be displayed to the user // - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. @@ -297,7 +299,7 @@ DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient DeviceLinkInteraction.displayTextAndPIN("Log in?") )); -// Init authentication session +// Initiate authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get authentication session request used for starting the authentication session and use it later to validate sessions status response @@ -315,7 +317,7 @@ URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); // Next steps: -// - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse +// - Generate QR-code or device link to be displayed to the user // - Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. @@ -435,7 +437,7 @@ String sessionSecret = signatureResponse.getSessionSecret(); Instant receivedAt = signatureResponse.getReceivedAt(); String deviceLinkBase = signatureResponse.getDeviceLinkBase(); -// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret, receivedAt and deviceLinkBase provided in the signatureResponse +// Generate QR-code or device link to be displayed to the user // Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. @@ -469,7 +471,7 @@ String sessionSecret = signatureResponse.getSessionSecret(); Instant receivedAt = signatureResponse.getReceivedAt(); String deviceLinkBase = signatureResponse.getDeviceLinkBase(); -// Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret, receivedAt and deviceLinkBase provided in the signatureResponse +// Generate QR-code or device link to be displayed to the user // Start querying sessions status ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. @@ -736,19 +738,18 @@ if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { ### Validating session status response It's important to validate the session status response to ensure that the returned signature or authentication result is valid. +For validating authentication session status response, use the `AuthenticationResponseValidator`. +For validating signature session status response, use the `SignatureResponseValidator`. +NB! Integrators must validate signature value against expected signature value. -* Validate that endResult is OK if the session was successful. -* Check the certificate field to ensure it has the required certificate level and that it is signed by a trusted CA. -* For `ACSP_V2` signature validation, compare the digest of the signature protocol, server random, and random challenge. -* For `RAW_DIGEST_SIGNATURE`, validate the signature against the expected digest. +#### Set up CertificateValidator -#### Example of validating the authentication sessions response: - -##### Authentication response validator setup - -###### Setup TrustedCACertStore +CertificateValidator will check if the certificate is not expired and is trusted +by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore. +Will be used by AuthenticationResponseValidator and SignatureResponseValidator. ```java +// Set up TrustedCACertStore // Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); @@ -768,21 +769,28 @@ TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() .withTrustAnchors(trustAnchors) .withIntermediateCACertificates(intermediateCACertificates) .build(); -``` -###### Setup AuthenticationResponseValidator -```java -TrustedCACertStore trustedCACertStore; -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(trustedCACertStore); +// Set up CertificateValidator with the trusted CA store +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); ``` -###### Validate sessions status +#### Example of validating the authentication sessions response: + +AuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) ```java -AuthenticationSessionRequest authenticationSessionRequest; -DeviceLinkSessionResponse sessionResponse; +// Set up AuthenticationResponseValidator with the CertificateValidator +AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); // get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID()); // validate sessions state is completed @@ -810,19 +818,24 @@ try { ``` #### Example of validating the signature session response: + +SignatureResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) ```java try { - // Get the session status response - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .build(); - - // Initialize the validator with the CA store - SignatureResponseValidator validator = new SignatureResponseValidator(trustedCACertStore); - + // Objects needed for validation + CertificateResponse certResponse; // queried by document number or from certificate choice session + SignableData signableData; // data that was sent for signing + // Initialize the signature response validator with CertificateValidator + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - SignatureResponse signatureResponse = validator.from(sessionStatus, "QUALIFIED"); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + // Validate signature value. This step can be skipped if other means of validating the signature value can be used. + SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certResponse.certificate(), + signatureResponse.getRsaSsaPssParameters()); // Process the response (e.g., save to database or pass to another system) handleSignatureResponse(signatureResponse); @@ -861,7 +874,10 @@ The session status response may return various error codes indicating the outcom ## Certificate by document number -In API v3.1, the flow to initiate a **notification-based certificate choice session using a document number** was removed. Instead, a new, simplified endpoint was introduced. +In API v3.1 new endpoint was introduced to simplify querying certificate for signing. +RP can directly query the user's signing certificate by document number — no session flow or user interaction required. +Can be used for device link and notification-based signature flows. +Only requirement is that the device link authentication is successfully completed before to get the document number. ### Request Parameters The request parameters for the certificate by document number request are as follows: @@ -878,23 +894,25 @@ The request parameters for the certificate by document number request are as fol * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` -### Get certificate using document number - -RP can directly query the user's signing certificate by document number — no session flow or user interaction required. - -#### Usage example in Java +### Example of querying certificate by document number ```java String documentNumber = "PNOLT-40504040001-MOCK-Q"; +// Build the certificate by document number request and query the certificate CertificateByDocumentNumberResult certResponse = smartIdClient .createCertificateByDocumentNumber() .withDocumentNumber(documentNumber) .getCertificateByDocumentNumber(); -// certResponse.certificate(); contains Base64-encoded certificate -// certResponse.certificateLevel(); is either ADVANCED or QUALIFIED +// Set up the certificate validator +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + +// Validate the certificate +certificateValidator.validateCertificate(certResponse.certificate()); ``` +Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). ## Notification-based flows diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index ab16a0a7..dd85ab92 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -90,7 +90,7 @@ public X509Certificate getAuthCertificate() { } /** - * Person date of birth. + * Person's date of birth. * NB! This information is not available for some Latvian certificates. * * @return Date of birth if this information is available in authentication response or empty optional. diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index 1f0b1b1d..9d8f4013 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -43,19 +43,14 @@ public class AuthenticationResponse { private String serverRandom; private String userChallenge; private String relyingPartyName; - private SignatureAlgorithm signatureAlgorithm; private String signatureValueInBase64; - private HashAlgorithm hashAlgorithm; private X509Certificate certificate; private AuthenticationCertificateLevel certificateLevel; private String documentNumber; private String interactionTypeUsed; private FlowType flowType; private String deviceIpAddress; - private MaskGenAlgorithm maskGenAlgorithm; - private HashAlgorithm maskHashAlgorithm; - private int saltLength; - private TrailerField trailerField; + private RsaSsaPssParameters rsaSsaPssSignatureParameters; public String getEndResult() { return endResult; @@ -87,14 +82,6 @@ public byte[] getSignatureValue() { } } - public SignatureAlgorithm getSignatureAlgorithm() { - return signatureAlgorithm; - } - - public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - public X509Certificate getCertificate() { return certificate; } @@ -167,43 +154,11 @@ public void setFlowType(FlowType flowType) { this.flowType = flowType; } - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } - - public MaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - public HashAlgorithm getMaskHashAlgorithm() { - return maskHashAlgorithm; - } - - public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { - this.maskHashAlgorithm = maskHashAlgorithm; - } - - public int getSaltLength() { - return saltLength; - } - - public void setSaltLength(int saltLength) { - this.saltLength = saltLength; - } - - public TrailerField getTrailerField() { - return trailerField; + public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { + return rsaSsaPssSignatureParameters; } - public void setTrailerField(TrailerField trailerField) { - this.trailerField = trailerField; + public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { + this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; } } diff --git a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java similarity index 92% rename from src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java rename to src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 573a2664..4d5f04da 100644 --- a/src/main/java/ee/sk/smartid/DefaultAuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -44,9 +44,9 @@ /** * Validates and maps the received session status to authentication response */ -public class DefaultAuthenticationResponseMapper implements AuthenticationResponseMapper { +public class AuthenticationResponseMapperImpl implements AuthenticationResponseMapper { - private static final Logger logger = LoggerFactory.getLogger(DefaultAuthenticationResponseMapper.class); + private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); private static AuthenticationResponseMapper instance; @@ -56,13 +56,13 @@ public class DefaultAuthenticationResponseMapper implements AuthenticationRespon public static AuthenticationResponseMapper getInstance() { if (instance == null) { - instance = new DefaultAuthenticationResponseMapper(); + instance = new AuthenticationResponseMapperImpl(); } return instance; } /** - * Maps session status to authentication response + * Maps session status to authentication response {@link AuthenticationResponse] * * @param sessionStatus session status received from Smart-ID server * @return authentication response @@ -81,28 +81,21 @@ public AuthenticationResponse from(SessionStatus sessionStatus) { authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); - authenticationResponse.setSignatureAlgorithm(signatureAlgorithm); var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - var hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null); - authenticationResponse.setHashAlgorithm(hashAlgorithm); - MaskGenAlgorithm maskGenAlgorithm = MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm()); - authenticationResponse.setMaskGenAlgorithm(maskGenAlgorithm); - var maskGenHashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null); - authenticationResponse.setMaskHashAlgorithm(maskGenHashAlgorithm); - authenticationResponse.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - TrailerField trailerField = TrailerField.fromString(signatureAlgorithmParameters.getTrailerField()); - authenticationResponse.setTrailerField(trailerField); + var rssSsaPssParameters = new RsaSsaPssParameters(); + rssSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm())); + rssSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rssSsaPssParameters.setTrailerField(TrailerField.fromString(signatureAlgorithmParameters.getTrailerField())); + authenticationResponse.setRsaSsaPssSignatureParameters(rssSsaPssParameters); authenticationResponse.setCertificate(toCertificate(sessionCertificate)); authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); - authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return authenticationResponse; } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index fd758a88..df8ad7f6 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -29,28 +29,12 @@ import static org.slf4j.LoggerFactory.getLogger; import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.cert.CertPathBuilder; -import java.security.cert.CertPathBuilderException; -import java.security.cert.CertStore; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; import java.security.cert.CertificateParsingException; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.PKIXCertPathBuilderResult; -import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; import java.util.Base64; import java.util.List; import java.util.Set; -import org.bouncycastle.asn1.x500.style.BCStyle; import org.slf4j.Logger; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; @@ -58,7 +42,6 @@ import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.CertificateAttributeUtil; import ee.sk.smartid.util.StringUtil; /** @@ -73,40 +56,49 @@ public class AuthenticationResponseValidator { private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; - private final TrustedCACertStore trustedCaCertStore; + private final CertificateValidator certificateValidator; + private final SignatureValueValidator signatureValueValidator; private final AuthenticationResponseMapper authenticationResponseMapper; /** - * Initializes the validator with a {@link TrustedCACertStore}. + * Creates an instance of {@link AuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} * - * @param trustedCaCertStore the store containing trusted CA certificates + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value */ - public AuthenticationResponseValidator(TrustedCACertStore trustedCaCertStore) { - this(trustedCaCertStore, DefaultAuthenticationResponseMapper.getInstance()); + public AuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; } /** - * Initializes the validator with a {@link TrustedCACertStore} and a custom {@link AuthenticationResponseMapper}. + * Creates an instance of {@link AuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} * - * @param trustedCaCertStore the store containing trusted CA certificates - * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @return a new instance of {@link AuthenticationResponseValidator} */ - public AuthenticationResponseValidator(TrustedCACertStore trustedCaCertStore, AuthenticationResponseMapper authenticationResponseMapper) { - this.trustedCaCertStore = trustedCaCertStore; - this.authenticationResponseMapper = authenticationResponseMapper; + public static AuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new AuthenticationResponseValidator(certificateValidator, + AuthenticationResponseMapperImpl.getInstance(), + SignatureValueValidatorImpl.getInstance()); } /** * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - *

- * This method sets brokeredRpName value to null * * @param sessionStatus the session status * @param authenticationSessionRequest the authentication session request * @param schemaName the schema name * @return the authentication identity */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + public AuthenticationIdentity validate(SessionStatus sessionStatus, + AuthenticationSessionRequest authenticationSessionRequest, + String schemaName) { return validate(sessionStatus, authenticationSessionRequest, schemaName, null); } @@ -119,7 +111,10 @@ public AuthenticationIdentity validate(SessionStatus sessionStatus, Authenticati * @param brokeredRpName the brokered relying party name * @return the authentication identity */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { + public AuthenticationIdentity validate(SessionStatus sessionStatus, + AuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { validateInputs(sessionStatus, authenticationSessionRequest, schemaName); AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); validateCertificate(authenticationResponse, AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel())); @@ -127,23 +122,10 @@ public AuthenticationIdentity validate(SessionStatus sessionStatus, Authenticati return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); } - private static void validateInputs(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("`sessionStatus` is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("`authenticationSessionRequest` is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("`schemaName` is not provided"); - } - } - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateIsCurrentlyValid(authenticationResponse.getCertificate()); - validateCertificateChain(authenticationResponse); - validateCertificatePurpose(authenticationResponse); validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + validateCertificatePurpose(authenticationResponse); } private void validateCertificatePurpose(AuthenticationResponse authenticationResponse) { @@ -180,18 +162,11 @@ private void validateSignature(AuthenticationResponse authenticationResponse, AuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { - try { - Signature result = getSignature(authenticationResponse); - result.initVerify(authenticationResponse.getCertificate().getPublicKey()); - result.update(constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName)); - byte[] signedHash = authenticationResponse.getSignatureValue(); - if (!result.verify(signedHash)) { - logger.error("Signature value does not match the calculated signature for authentication response"); - throw new UnprocessableSmartIdResponseException("Failed to verify validity of authentication signature returned by Smart-ID"); - } - } catch (GeneralSecurityException ex) { - throw new UnprocessableSmartIdResponseException("Authentication signature validation failed", ex); - } + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); } private byte[] constructPayload(AuthenticationResponse authenticationResponse, @@ -216,55 +191,15 @@ private byte[] constructPayload(AuthenticationResponse authenticationResponse, .getBytes(StandardCharsets.UTF_8); } - private void validateCertificateChain(AuthenticationResponse authenticationResponse) { - try { - PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ - setCertificate(authenticationResponse.getCertificate()); - }}); - CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); - params.addCertStore(intermediateStore); - params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); - CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); - PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); - - if (logger.isDebugEnabled()) { - X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); - X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); - X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", - CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); - } - } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { - throw new UnprocessableSmartIdResponseException("Authentication certificate chain validation failed", ex); + private static void validateInputs(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); } - } - - private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - logger.error("Authentication certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); - throw new UnprocessableSmartIdResponseException("Authentication certificate is invalid", ex); + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); } - } - - private static Signature getSignature(AuthenticationResponse authenticationResponse) { - try { - var params = new PSSParameterSpec(authenticationResponse.getHashAlgorithm().getAlgorithmName(), - authenticationResponse.getMaskGenAlgorithm().getMgfName(), - new MGF1ParameterSpec(authenticationResponse.getMaskHashAlgorithm().getAlgorithmName()), - authenticationResponse.getSaltLength(), - authenticationResponse.getTrailerField().getPssSpecValue()); - var signature = Signature.getInstance(authenticationResponse.getSignatureAlgorithm().getAlgorithmName()); - signature.setParameter(params); - return signature; - } catch (NoSuchAlgorithmException ex) { - logger.error("Invalid signature algorithm was provided: {}", authenticationResponse.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); - } catch (InvalidAlgorithmParameterException ex) { - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); } } diff --git a/src/main/java/ee/sk/smartid/CertificateValidator.java b/src/main/java/ee/sk/smartid/CertificateValidator.java new file mode 100644 index 00000000..54063677 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateValidator.java @@ -0,0 +1,44 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public interface CertificateValidator { + + /** + * Validates the given X509 certificate. + *

+ * This method checks if the certificate is not expired and can be trusted + * + * @param certificate the X509Certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is invalid + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java new file mode 100644 index 00000000..0d21d9ae --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java @@ -0,0 +1,98 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +public class CertificateValidatorImpl implements CertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); + + private final TrustedCACertStore trustedCaCertStore; + + public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { + this.trustedCaCertStore = trustedCaCertStore; + } + + @Override + public void validate(X509Certificate certificate) { + validateCertificateIsCurrentlyValid(certificate); + validateCertificateChain(certificate); + } + + private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Certificate is invalid", ex); + } + } + + private void validateCertificateChain(X509Certificate certificate) { + try { + PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ + setCertificate(certificate); + }}); + CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); + params.addCertStore(intermediateStore); + params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); + + if (logger.isDebugEnabled()) { + X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); + X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); + X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", + CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); + } + } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { + throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 30a910b2..ec2fb95a 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,11 +29,9 @@ import java.util.List; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; @@ -50,7 +48,6 @@ public class DeviceLinkSignatureSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(DeviceLinkSignatureSessionRequestBuilder.class); private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; private final SmartIdConnector connector; @@ -243,7 +240,7 @@ public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String in } /** - * Sends the signature request and initiates a device link-based signature session. + * Sends the signature request and initiates a device link based signature session. *

* There are two supported ways to start the signature session: *

    @@ -253,7 +250,7 @@ public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String in * * @return a {@link DeviceLinkSessionResponse} containing session details such as * session ID, session token, session secret and device link base URL. - * @throws SmartIdClientException if request parameters are invalid + * @throws SmartIdClientException if request parameters are invalid * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initSignatureSession() { @@ -270,61 +267,45 @@ private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest r } else if (semanticsIdentifier != null) { return connector.initDeviceLinkSignature(request, semanticsIdentifier); } else { - throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set. Anonymous signing is not allowed."); + throw new SmartIdClientException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed."); } } private SignatureSessionRequest createSignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - if (signableHash != null || signableData != null) { - signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); - } - signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - - var signatureAlgorithmParameters = new SignatureAlgorithmParameters(hashAlgorithm.getValue()); - signatureProtocolParameters.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - - request.setSignatureProtocolParameters(signatureProtocolParameters); - - request.setNonce(nonce); - request.setInteractions(DeviceLinkUtil.encodeToBase64(interactions)); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - request.setCapabilities(capabilities); - request.setInitialCallbackUrl(initialCallbackUrl); - return request; + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + SignatureUtil.getDigestToSignBase64(signableHash, signableData), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(hashAlgorithm.getValue())); + return new SignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce != null ? nonce : null, + capabilities, + DeviceLinkUtil.encodeToBase64(interactions), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl); } private void validateParameters() { if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { - throw new SmartIdClientException("Relying Party UUID must be set."); + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } if (relyingPartyName == null || relyingPartyName.isEmpty()) { - throw new SmartIdClientException("Relying Party Name must be set."); + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } validateInteractions(); validateInitialCallbackUrl(); if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); + throw new SmartIdClientException("Value for 'nonce' length must be between 1 and 30 characters."); } } private void validateInteractions() { if (interactions == null || interactions.isEmpty()) { - logger.error("Parameter interactions must be set and contain at least one interaction."); - throw new SmartIdClientException("Parameter interactions must be set and contain at least one interaction."); + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } validateNoDuplicateInteractions(); interactions.forEach(DeviceLinkInteraction::validate); @@ -332,35 +313,30 @@ private void validateInteractions() { private void validateInitialCallbackUrl() { if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + throw new SmartIdClientException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionToken())) { - logger.error("Session token is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionSecret())) { - logger.error("Session secret is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); } if (deviceLinkSignatureSessionResponse.getDeviceLinkBase() == null || deviceLinkSignatureSessionResponse.getDeviceLinkBase().toString().isBlank()) { - logger.error("deviceLinkBase is missing or empty in the response"); - throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); } } private void validateNoDuplicateInteractions() { if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { - logger.error("Duplicate values found in interactions"); - throw new SmartIdClientException("Duplicate values in interactions are not allowed"); + throw new SmartIdClientException("Value for 'interactions' cannot contain duplicate types"); } } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 3b149975..77b37953 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -35,12 +35,14 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; import ee.sk.smartid.util.NotificationUtil; @@ -63,6 +65,7 @@ public class NotificationSignatureSessionRequestBuilder { private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA_512; private SignableData signableData; private SignableHash signableHash; @@ -244,30 +247,22 @@ private NotificationSignatureSessionResponse initSignatureSession(SignatureSessi } private SignatureSessionRequest createSignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - if (signableHash != null || signableData != null) { - signatureProtocolParameters.setDigest(SignatureUtil.getDigestToSignBase64(signableHash, signableData)); - } - signatureProtocolParameters.setSignatureAlgorithm(signatureAlgorithm.getAlgorithmName()); - request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setNonce(nonce); - request.setInteractions(NotificationUtil.encodeToBase64(allowedInteractionsOrder)); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - - request.setCapabilities(capabilities); - return request; + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + SignatureUtil.getDigestToSignBase64(signableHash, signableData), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(hashAlgorithm.getValue())); + + return new SignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce, + capabilities, + NotificationUtil.encodeToBase64(allowedInteractionsOrder), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + null + ); } private void validateParameters() { diff --git a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java new file mode 100644 index 00000000..68780813 --- /dev/null +++ b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java @@ -0,0 +1,82 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public class RsaSsaPssParameters { + + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + + private HashAlgorithm digestHashAlgorithm; + private MaskGenAlgorithm maskGenAlgorithm; + private HashAlgorithm maskHashAlgorithm; + private int saltLength; + private TrailerField trailerField; + + public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { + this.digestHashAlgorithm = digestHashAlgorithm; + } + + public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { + this.maskHashAlgorithm = maskHashAlgorithm; + } + + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + public void setTrailerField(TrailerField trailerField) { + this.trailerField = trailerField; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public HashAlgorithm getDigestHashAlgorithm() { + return digestHashAlgorithm; + } + + public MaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + public HashAlgorithm getMaskHashAlgorithm() { + return maskHashAlgorithm; + } + + public int getSaltLength() { + return saltLength; + } + + public TrailerField getTrailerField() { + return trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index 96ae4218..0b5b5e15 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -38,11 +38,6 @@ public class SignatureResponse implements Serializable { private String signatureValueInBase64; private String algorithmName; private SignatureAlgorithm signatureAlgorithm; - private HashAlgorithm hashAlgorithm; - private MaskGenAlgorithm maskGenAlgorithm; - private HashAlgorithm maskHashAlgorithm; - private int saltLength; - private TrailerField trailerField; private FlowType flowType; private X509Certificate certificate; private String requestedCertificateLevel; @@ -50,6 +45,7 @@ public class SignatureResponse implements Serializable { private String documentNumber; private String interactionFlowUsed; private String deviceIpAddress; + private RsaSsaPssParameters rsaSsaPssParameters; public byte[] getSignatureValue() { try { @@ -92,46 +88,6 @@ public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } - - public MaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - public HashAlgorithm getMaskHashAlgorithm() { - return maskHashAlgorithm; - } - - public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { - this.maskHashAlgorithm = maskHashAlgorithm; - } - - public int getSaltLength() { - return saltLength; - } - - public void setSaltLength(int saltLength) { - this.saltLength = saltLength; - } - - public TrailerField getTrailerField() { - return trailerField; - } - - public void setTrailerField(TrailerField trailerField) { - this.trailerField = trailerField; - } - public FlowType getFlowType() { return flowType; } @@ -187,4 +143,12 @@ public String getDeviceIpAddress() { public void setDeviceIpAddress(String deviceIpAddress) { this.deviceIpAddress = deviceIpAddress; } + + public RsaSsaPssParameters getRsaSsaPssParameters() { + return rsaSsaPssParameters; + } + + public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { + this.rsaSsaPssParameters = rsaSsaPssParameters; + } } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index 6d318edc..25bd79ad 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -26,17 +26,9 @@ * #L% */ -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertPathBuilder; -import java.security.cert.CertPathBuilderException; -import java.security.cert.CertStore; +import java.io.IOException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.PKIXCertPathBuilderResult; -import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashSet; @@ -62,14 +54,12 @@ import ee.sk.smartid.exception.useraction.UserRefusedException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; import ee.sk.smartid.rest.dao.SessionResult; import ee.sk.smartid.rest.dao.SessionSignature; import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.util.StringUtil; -//TODO: review this class for possible refactoring and improvements - 2025-07-08 public class SignatureResponseValidator { private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); @@ -77,19 +67,23 @@ public class SignatureResponseValidator { private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); private static final Set QUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); private static final Set NONQUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); - private static final String QC_STATEMENT_OID = "0.4.0.1862.1.6.1"; private static final int KEYUSAGE_NON_REPUDIATION_INDEX = 1; + private static final String QC_STATEMENT_OID = "1.3.6.1.5.5.7.1.3"; + private static final String ELECTRONIC_SIGNING = "0.4.0.1862.1.6.1"; - private final TrustedCACertStore trustedCaCertStore; + private final CertificateValidator certificateValidator; private final boolean qcStatementRequired; - public SignatureResponseValidator(TrustedCACertStore store, boolean qcRequired) { - this.trustedCaCertStore = store; + public SignatureResponseValidator(CertificateValidator certificateValidator, boolean qcRequired) { + this.certificateValidator = certificateValidator; this.qcStatementRequired = qcRequired; } - public SignatureResponseValidator(TrustedCACertStore store) { - this(store, false); + /** + * Initializes the validator with a {@link CertificateValidator}. + */ + public SignatureResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, false); } /** @@ -103,10 +97,10 @@ public SignatureResponseValidator(TrustedCACertStore store) { * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. - * @throws SmartIdClientException if session status is missing, incomplete or inconsistent + * @throws SmartIdClientException if any of method parameters are not provided */ - public SignatureResponse from(SessionStatus sessionStatus, - String requestedCertificateLevel + public SignatureResponse validate(SessionStatus sessionStatus, + String requestedCertificateLevel ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { validateSessionsStatus(sessionStatus, requestedCertificateLevel); @@ -114,21 +108,19 @@ public SignatureResponse from(SessionStatus sessionStatus, SessionSignature sessionSignature = sessionStatus.getSignature(); SessionCertificate certificate = sessionStatus.getCert(); - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(sessionSignature.getSignatureAlgorithm()); - HashAlgorithm hashAlgorithm = HashAlgorithm.fromString(sessionSignature.getSignatureAlgorithmParameters().getHashAlgorithm()).orElse(null); - SessionMaskGenAlgorithm maskGenAlgorithm = sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm(); - HashAlgorithm maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()).orElse(null); - var signatureResponse = new SignatureResponse(); signatureResponse.setEndResult(sessionResult.getEndResult()); signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); - signatureResponse.setSignatureAlgorithm(signatureAlgorithm); - signatureResponse.setHashAlgorithm(hashAlgorithm); - signatureResponse.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); - signatureResponse.setMaskHashAlgorithm(maskGenHashAlgorithm); - signatureResponse.setSaltLength(sessionSignature.getSignatureAlgorithmParameters().getSaltLength()); signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - signatureResponse.setTrailerField(TrailerField.OXBC); + + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var rsaSsaPssParams = new RsaSsaPssParameters(); + rsaSsaPssParams.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rsaSsaPssParams.setTrailerField(TrailerField.OXBC); + signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); signatureResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); @@ -143,11 +135,11 @@ public SignatureResponse from(SessionStatus sessionStatus, private void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { if (sessionStatus == null) { - throw new SmartIdClientException("Session status was not provided"); + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); } if (StringUtil.isEmpty(sessionStatus.getState())) { - throw new UnprocessableSmartIdResponseException("State parameter is missing in session status"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'state' is empty"); } if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { @@ -161,30 +153,24 @@ private void validateSessionResult(SessionStatus sessionStatus, String requested SessionResult sessionResult = sessionStatus.getResult(); if (sessionResult == null) { - logger.error("Result is missing in the session status response"); - throw new UnprocessableSmartIdResponseException("Result is missing in the session status response"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'result' is missing"); } String endResult = sessionResult.getEndResult(); if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.endResult' is empty"); } if ("OK".equalsIgnoreCase(endResult)) { - logger.info("Session completed successfully"); - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Document number is missing in the session result"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.documentNumber' is empty"); } - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("InteractionFlowUsed is missing in the session status"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'interactionTypeUsed' is empty"); } - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Signature protocol is missing in session status"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' is empty"); } - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); validateSignature(sessionStatus); } else { @@ -193,22 +179,24 @@ private void validateSessionResult(SessionStatus sessionStatus, String requested } private void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { - if (sessionCertificate == null || StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Missing certificate in session response"); + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing or empty"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); } if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate level is missing in certificate"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); } X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); - if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { + logger.error("Signature session status certificate level mismatch: requested {}, returned {}", requestedCertificateLevel, sessionCertificate.getCertificateLevel()); throw new CertificateLevelMismatchException(); } - + certificateValidator.validate(certificate); validateCertificatePoliciesAndPurpose(certificate); - validateCertificateChain(certificate); } private void validateCertificatePoliciesAndPurpose(X509Certificate cert) { @@ -229,27 +217,6 @@ private void validateCertificatePoliciesAndPurpose(X509Certificate cert) { } } - private void validateCertificateChain(X509Certificate certificate) { - try { - var x509CertSelector = new X509CertSelector(); - x509CertSelector.setCertificate(certificate); - - var params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), x509CertSelector); - - CertStore intermediates = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); - params.addCertStore(intermediates); - params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); - - CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); - PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); - - logger.debug("Signature certificate validated. Trust anchor: {}", result.getTrustAnchor().getTrustedCert().getSubjectX500Principal()); - - } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { - throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); - } - } - private static X509Certificate parseAndCheckCertificate(String certBase64) { X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); try { @@ -282,14 +249,14 @@ private static Set getPolicyOids(X509Certificate certificate) { result.add(pi.getPolicyIdentifier().getId()); } } - } catch (Exception e) { - logger.debug("Unable to parse CertificatePolicies", e); + } catch (IOException ex) { + throw new UnprocessableSmartIdResponseException("Unable to parse certificate policies", ex); } return result; } private static boolean containsQcStatement(X509Certificate cert) { - byte[] extensionValue = cert.getExtensionValue("1.3.6.1.5.5.7.1.3"); + byte[] extensionValue = cert.getExtensionValue(QC_STATEMENT_OID); if (extensionValue == null) { return false; } @@ -299,13 +266,13 @@ private static boolean containsQcStatement(X509Certificate cert) { ASN1Sequence seq = (ASN1Sequence) ais2.readObject(); for (int i = 0; i < seq.size(); i++) { QCStatement st = QCStatement.getInstance(seq.getObjectAt(i)); - if (QC_STATEMENT_OID.equals(st.getStatementId().getId())) { + if (ELECTRONIC_SIGNING.equals(st.getStatementId().getId())) { return true; } } } - } catch (Exception ex) { - logger.debug("Unable to parse QCStatements", ex); + } catch (IOException ex) { + throw new UnprocessableSmartIdResponseException("Unable to parse QCStatements", ex); } return false; } @@ -316,115 +283,121 @@ private static void validateSignature(SessionStatus sessionStatus) { if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { validateRawDigestSignature(sessionStatus); } else { - throw new UnprocessableSmartIdResponseException("Unknown signature protocol: " + signatureProtocol); + logger.error("Signature session status field 'signatureProtocol' has unsupported value: {}", signatureProtocol); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' has unsupported value"); } } private static void validateRawDigestSignature(SessionStatus sessionStatus) { SessionSignature signature = sessionStatus.getSignature(); if (signature == null) { - throw new UnprocessableSmartIdResponseException("Signature object is missing"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature' is missing"); } validateSignatureValue(signature.getValue()); validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); validateFlowType(signature.getFlowType()); validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); - - logger.info("RAW_DIGEST_SIGNATURE fields successfully validated."); } private static void validateSignatureValue(String value) { - if (StringUtil.isEmpty(value) || !BASE64_PATTERN.matcher(value).matches()) { - throw new UnprocessableSmartIdResponseException("Signature value is missing or not Base64"); + if (StringUtil.isEmpty(value)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' is empty"); + } + if (!BASE64_PATTERN.matcher(value).matches()) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' does not have Base64-encoded value"); } } private static void validateSignatureAlgorithmName(String signatureAlgorithm) { if (StringUtil.isEmpty(signatureAlgorithm)) { - throw new UnprocessableSmartIdResponseException("Signature algorithm is missing"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' is missing"); } if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); - throw new UnprocessableSmartIdResponseException("Unexpected signature algorithm. Expected one of: " + possibleValues + ", but got: " + signatureAlgorithm); + logger.error("Signature session status field 'signature.signatureAlgorithm' has unsupported value: {}. Possible values: {}", signatureAlgorithm, possibleValues); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' has unsupported value"); } } private static void validateFlowType(String flowType) { if (StringUtil.isEmpty(flowType)) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.flowType` is empty"); + throw new UnprocessableSmartIdResponseException("Signature session status field `signature.flowType` is empty"); } if (!FlowType.isSupported(flowType)) { - logger.error("Invalid `signature.flowType` in session status: {}", flowType); - throw new UnprocessableSmartIdResponseException("Invalid `signature.flowType` in session status"); + logger.error("Signature session status field `signature.flowType` has invalid value: {}", flowType); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.flowType' has unsupported value"); } } private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { if (sessionSignatureAlgorithmParameters == null) { - throw new UnprocessableSmartIdResponseException("SignatureAlgorithmParameters is missing"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters' is missing"); } if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); } Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); if (hashAlgorithm.isEmpty()) { - logger.error("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status"); + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); } var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); } if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); } if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status"); + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"); } - if (maskGenAlgorithm.getParameters() == null || StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' is empty"); + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); } Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); if (mgfHashAlgorithm.isEmpty()) { - logger.error("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status"); + logger.error("Signature session 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); } if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { - logger.error("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm': expected {}, got {}", + logger.error("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm'"); + throw new UnprocessableSmartIdResponseException("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); } if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); } int expectedSaltLength = hashAlgorithm.get().getOctetLength(); int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); - if (expectedSaltLength != actualSaltLength) { - logger.error("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status: expected {}, got {}", expectedSaltLength, actualSaltLength); - throw new UnprocessableSmartIdResponseException("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status"); + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected {}, got {}", expectedSaltLength, actualSaltLength); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); } if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); } if (!TrailerField.OXBC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { - logger.error("Invalid `signature.signatureAlgorithmParameters.trailerField` in session status: {}", sessionSignatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status"); + logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); } } } diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidator.java b/src/main/java/ee/sk/smartid/SignatureValueValidator.java new file mode 100644 index 00000000..628518cc --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureValueValidator.java @@ -0,0 +1,46 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +public interface SignatureValueValidator { + + /** + * Validates the signature value against the calculated signature value. + * + * @param signatureValue the signature value to validate + * @param payload the original data that was signed + * @param certificate X509 certificate used for signature validation + * @param rsaSsaPssParameters signature parameters used for creating signature value + * @throws UnsupportedOperationException when there are any issue with validating the signature value + */ + void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters); +} diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java new file mode 100644 index 00000000..c016c7b1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -0,0 +1,112 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public final class SignatureValueValidatorImpl implements SignatureValueValidator { + + private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); + + private static SignatureValueValidatorImpl INSTANCE; + + private SignatureValueValidatorImpl() { + } + + public static SignatureValueValidatorImpl getInstance() { + if (INSTANCE == null) { + INSTANCE = new SignatureValueValidatorImpl(); + } + return INSTANCE; + } + + @Override + public void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + validateInputs(signatureValue, payload, certificate, rsaSsaPssParameters); + try { + Signature result = getSignature(rsaSsaPssParameters); + result.initVerify(certificate.getPublicKey()); + result.update(payload); + if (!result.verify(signatureValue)) { + throw new UnprocessableSmartIdResponseException("Provided signature value does not match the calculated signature value"); + } + } catch (GeneralSecurityException ex) { + throw new UnprocessableSmartIdResponseException("Signature value validation failed", ex); + } + } + + private Signature getSignature(RsaSsaPssParameters rsaSsaPssParameters) { + try { + var params = new PSSParameterSpec(rsaSsaPssParameters.getDigestHashAlgorithm().getAlgorithmName(), + rsaSsaPssParameters.getMaskGenAlgorithm().getMgfName(), + new MGF1ParameterSpec(rsaSsaPssParameters.getMaskHashAlgorithm().getAlgorithmName()), + rsaSsaPssParameters.getSaltLength(), + rsaSsaPssParameters.getTrailerField().getPssSpecValue()); + var signature = Signature.getInstance(rsaSsaPssParameters.getSignatureAlgorithm().getAlgorithmName()); + signature.setParameter(params); + return signature; + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", rsaSsaPssParameters.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } catch (InvalidAlgorithmParameterException ex) { + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); + } + } + + private static void validateInputs(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + if (signatureValue == null) { + throw new SmartIdClientException("Parameter 'signatureValue' is not provided"); + } + if (payload == null) { + throw new SmartIdClientException("Parameter 'payload' is not provided"); + } + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + if (rsaSsaPssParameters == null) { + throw new SmartIdClientException("Parameter 'rsaSsaPssParameters' is not provided"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index 07c8281a..e9591a73 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,33 +28,7 @@ import java.io.Serializable; -public class RawDigestSignatureProtocolParameters implements Serializable { - - private String digest; - private String signatureAlgorithm; - private SignatureAlgorithmParameters signatureAlgorithmParameters; - - public String getDigest() { - return digest; - } - - public void setDigest(String digest) { - this.digest = digest; - } - - public String getSignatureAlgorithm() { - return signatureAlgorithm; - } - - public void setSignatureAlgorithm(String signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - public SignatureAlgorithmParameters getSignatureAlgorithmParameters() { - return signatureAlgorithmParameters; - } - - public void setSignatureAlgorithmParameters(SignatureAlgorithmParameters signatureAlgorithmParameters) { - this.signatureAlgorithmParameters = signatureAlgorithmParameters; - } +public record RawDigestSignatureProtocolParameters(String digest, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java index 63a510f0..7f40fce3 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,108 +30,15 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.SignatureProtocol; -public class SignatureSessionRequest implements Serializable { - - private String relyingPartyUUID; - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - private final String signatureProtocol = SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); - - private RawDigestSignatureProtocolParameters signatureProtocolParameters; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String interactions; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private String initialCallbackUrl; - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getSignatureProtocol() { - return signatureProtocol; - } - - public RawDigestSignatureProtocolParameters getSignatureProtocolParameters() { - return signatureProtocolParameters; - } - - public void setSignatureProtocolParameters(RawDigestSignatureProtocolParameters signatureProtocolParameters) { - this.signatureProtocolParameters = signatureProtocolParameters; - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public String getInteractions() { - return interactions; - } - - public void setInteractions(String interactions) { - this.interactions = interactions; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - - public String getInitialCallbackUrl() { - return initialCallbackUrl; - } - - public void setInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - } +public record SignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java similarity index 99% rename from src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java rename to src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index 5401c49a..97996627 100644 --- a/src/test/java/ee/sk/smartid/DefaultAuthenticationResponseMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -56,7 +56,7 @@ import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; -class DefaultAuthenticationResponseMapperTest { +class AuthenticationResponseMapperImplTest { private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); @@ -64,7 +64,7 @@ class DefaultAuthenticationResponseMapperTest { @BeforeEach void setUp() { - authenticationResponseMapper = DefaultAuthenticationResponseMapper.getInstance(); + authenticationResponseMapper = AuthenticationResponseMapperImpl.getInstance(); } @Test @@ -125,8 +125,8 @@ void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorit assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - assertEquals(hashAlgorithm, authenticationResponse.getHashAlgorithm()); - assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getSaltLength()); + assertEquals(hashAlgorithm, authenticationResponse.getRsaSsaPssSignatureParameters().getDigestHashAlgorithm()); + assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getRsaSsaPssSignatureParameters().getSaltLength()); } @Test diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index e7f8926f..9d312150 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -29,11 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.time.LocalDate; import java.util.Base64; import java.util.List; @@ -65,8 +61,6 @@ class AuthenticationResponseValidatorTest { private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - private static final String UNTRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); private AuthenticationResponseValidator authenticationResponseValidator; @@ -74,7 +68,8 @@ class AuthenticationResponseValidatorTest { @BeforeEach void setUp() { TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - authenticationResponseValidator = new AuthenticationResponseValidator(trustedCaCertStore); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + authenticationResponseValidator = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); } @Disabled("Can make this work when TEST numbers will be available in the DEMO env") @@ -116,20 +111,20 @@ class ValidateInputs { @Test void validate_sessionStatusNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("`sessionStatus` is not provided", ex.getMessage()); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); } @Test void validate_authenticationSessionRequestIsNotProvided_throwException() { var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); - assertEquals("`authenticationSessionRequest` is not provided", ex.getMessage()); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_emptySchemaNameIsProvided_throwException(String schemaName) { var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); - assertEquals("`schemaName` is not provided", ex.getMessage()); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); } } @@ -142,24 +137,6 @@ void validate_sessionStatusResultIsNotProvided_throwException() { @Nested class ValidateSessionStatusCertificate { - @Test - void validate_certificateExpired_throwException() { - var sessionStatus = toSessionsStatus(EXPIRED_CERT, "QUALIFIED", ""); - - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - - assertEquals("Authentication certificate is invalid", ex.getMessage()); - } - - @Test - void validate_certificateIsNotTrusted_throwException() { - var sessionStatus = toSessionsStatus(UNTRUSTED_CERT, "QUALIFIED", ""); - - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Authentication certificate chain validation failed", ex.getMessage()); - } - @Test void validate_certificateLevelLowerThanRequested_throwException() { var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", ""); @@ -170,7 +147,7 @@ void validate_certificateLevelLowerThanRequested_throwException() { } @Test - void validate_certificateCannotBeForAuthentication_throwException() { + void validate_certificateCannotBeUsedForAuthentication_throwException() { var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", ""); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); @@ -189,7 +166,7 @@ void validate_invalidSignature_throwException() { var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - assertEquals("Authentication signature validation failed", ex.getMessage()); + assertEquals("Signature value validation failed", ex.getMessage()); } } @@ -246,14 +223,6 @@ private static AuthenticationSessionRequest toAuthenticationSessionRequest(Strin null); } - private X509Certificate toX509Certificate(String certificate) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } private static String toBase64(String data) { return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); @@ -264,8 +233,4 @@ private static String getEncodedCertificateData(String certificate) { .replace("-----END CERTIFICATE-----", "") .replace("\n", ""); } - - private static String toUrlSafeBase64(String data) { - return Base64.getUrlEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } } diff --git a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java new file mode 100644 index 00000000..9808a1f8 --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java @@ -0,0 +1,81 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class CertificateValidatorImplTest { + + private static final String TRUSTED_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String NOT_TRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private CertificateValidatorImpl certificateValidator; + + @BeforeEach + void setUp() { + certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build()); + } + + @Test + void validate_ok() throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + assertDoesNotThrow(() -> certificateValidator.validate(certificate)); + } + + @Test + void validate_expired() throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + certificateValidator.validate(certificate); + }); + assertEquals("Certificate is invalid", exception.getMessage()); + } + + @Test + void validate_notTrusted() throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { + certificateValidator.validate(certificate); + }); + assertEquals("Certificate chain validation failed", exception.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index f2713424..c5cf9d9c 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -101,7 +101,7 @@ void initSignatureSession_withSemanticsIdentifier() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @Test @@ -122,7 +122,7 @@ void initSignatureSession_withDocumentNumber() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(documentNumber)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @ParameterizedTest @@ -140,8 +140,8 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.getCertificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); + assertEquals(expectedValue, request.certificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @ParameterizedTest @@ -159,8 +159,8 @@ void initSignatureSession_withNonce_ok(String nonce) { verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(nonce, request.getNonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); + assertEquals(nonce, request.nonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @Test @@ -177,9 +177,9 @@ void initSignatureSession_withRequestProperties() { verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.getRequestProperties()); - assertTrue(capturedRequest.getRequestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertNotNull(capturedRequest.requestProperties()); + assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test @@ -198,9 +198,9 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @ParameterizedTest @@ -220,9 +220,9 @@ void initSignatureSession_withSignableHash(HashType hashType) { verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @ParameterizedTest @@ -240,8 +240,8 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test @@ -260,7 +260,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @Nested @@ -271,7 +271,7 @@ void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { builder.withDocumentNumber(null).withSemanticsIdentifier(null); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Either documentNumber or semanticsIdentifier must be set. Anonymous signing is not allowed.", ex.getMessage()); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed.", ex.getMessage()); } @Test @@ -300,12 +300,12 @@ void initSignatureSession_whenInteractionsIsNullOrEmpty(List builder.initSignatureSession()); - assertEquals("Parameter interactions must be set and contain at least one interaction.", ex.getMessage()); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { var exception = assertThrows(SmartIdClientException.class, () -> new DeviceLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -318,7 +318,7 @@ void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url, .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) .initSignatureSession() ); - assertEquals(expectedErrorMessage, exception.getMessage()); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); } @ParameterizedTest @@ -335,7 +335,7 @@ void initSignatureSession_duplicateInteractions_shouldThrowException(List builder.initSignatureSession()); - assertEquals("Relying Party UUID must be set.", ex.getMessage()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } @ParameterizedTest @@ -353,21 +353,21 @@ void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { builder.withRelyingPartyName(relyingPartyName); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Relying Party Name must be set.", ex.getMessage()); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } @Test void initSignatureSession_invalidNonce() { builder.withNonce("1234567890123456789012345678901"); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); } @Test void initSignatureSession_emptyNonce() { builder.withNonce(""); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); } @Test @@ -396,7 +396,7 @@ void validateResponseParameters_missingSessionID(String sessionID) { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("Session ID is missing from the response", ex.getMessage()); + assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -412,7 +412,7 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("Session token is missing from the response", ex.getMessage()); + assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -428,7 +428,7 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("Session secret is missing from the response", ex.getMessage()); + assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -444,7 +444,7 @@ void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String d when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("deviceLinkBase is missing from the response", ex.getMessage()); + assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); } } @@ -490,9 +490,9 @@ private static class InvalidInitialCallbackUrlArgumentProvider implements Argume @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") ); } } diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index 11df2505..af66b5aa 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -98,7 +98,7 @@ void initSignatureSession_withSemanticsIdentifier() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), eq(semanticsIdentifier)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @Test @@ -118,7 +118,7 @@ void initSignatureSession_withDocumentNumber() { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), eq(documentNumber)); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().getSignatureProtocol()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @ParameterizedTest @@ -136,8 +136,8 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.getCertificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); + assertEquals(expectedValue, request.certificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @ParameterizedTest @@ -155,8 +155,8 @@ void initSignatureSession_withNonce_ok(String nonce) { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(nonce, request.getNonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.getSignatureProtocol()); + assertEquals(nonce, request.nonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @Test @@ -173,8 +173,8 @@ void withSignatureAlgorithm_setsCorrectAlgorithm() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); } @Test @@ -191,9 +191,9 @@ void initSignatureSession_withRequestProperties() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.getRequestProperties()); - assertTrue(capturedRequest.getRequestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertNotNull(capturedRequest.requestProperties()); + assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Disabled("Signature algorithm has changed") @@ -214,9 +214,9 @@ void initSignatureSession_withSignableHash(HashType hashType) { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @ParameterizedTest @@ -234,8 +234,8 @@ void initSignatureSession_withCapabilities(Set capabilities, Set verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.getCapabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @ParameterizedTest @@ -254,9 +254,9 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.getSignatureProtocolParameters().getDigest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.getSignatureProtocol()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test @@ -274,7 +274,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @Test @@ -290,7 +290,7 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignableDataOrHash() { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.getSignatureProtocolParameters().getSignatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @Nested diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index 36af0b8d..2f23db3c 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -37,6 +37,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; @@ -61,125 +62,112 @@ void setUp() { TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() .withOcspEnabled(false) .build(); - signatureResponseValidator = new SignatureResponseValidator(trustedCaCertStore); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + signatureResponseValidator = new SignatureResponseValidator(certificateValidator); } @Test - void from_stateParameterMissing() { + void validate_stateParameterMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setState(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("State parameter is missing in session status", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'state' is empty", ex.getMessage()); } @Test - void from_sessionNotComplete() { + void validate_sessionNotComplete() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setState("RUNNING"); - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); assertTrue(ex.getMessage().contains("Session is not complete")); } @Test - void from_sessionResultNull() { + void validate_sessionResultNull() { SessionStatus sessionStatus = new SessionStatus(); sessionStatus.setState("COMPLETE"); sessionStatus.setResult(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("Result is missing in the session status response", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'result' is missing", ex.getMessage()); } @Test - void from_sessionResultNull_throwsException() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - - sessionStatus.setResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Result is missing in the session status response", ex.getMessage()); - } - - @Test - void from_missingDocumentNumber() { + void validate_missingDocumentNumber() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getResult().setDocumentNumber(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("Document number is missing in the session result", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); } @Test - void from_missingInteractionFlowUsed() { + void validate_missingInteractionFlowUsed() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setInteractionTypeUsed(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("InteractionFlowUsed is missing in the session status", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); } @Test - void from_signatureProtocolMissing() { + void validate_signatureProtocolMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature protocol is missing in session status", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); } @Nested class CertificateValidation { @Test - void from_missingCertificate() { + void validate_missingCertificate() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setCert(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("Missing certificate in session response", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'cert' is missing or empty", ex.getMessage()); } @Test - void from_missingCertificateValue() { + void validate_missingCertificateValue() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setValue(null); - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("Missing certificate in session response", ex.getMessage()); + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); } @Test - void from_certificateLevelMissing() { + void validate_certificateLevelMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setCertificateLevel(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Certificate level is missing in certificate", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); } @Test - void from_certificateLevelMismatch() { + void validate_certificateLevelMismatch() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setCertificateLevel("ADVANCED"); - var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); } @Test - void from_qcStatementRequiredButMissing() { - var validatorWithQcRequired = new SignatureResponseValidator(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(), true); + void validate_qcStatementRequiredButMissing() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + var validatorWithQcRequired = new SignatureResponseValidator(certificateValidator, true); SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validatorWithQcRequired.from(sessionStatus, "QUALIFIED")); - + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validatorWithQcRequired.validate(sessionStatus, "QUALIFIED")); assertEquals("QCStatement 0.4.0.1862.1.6.1 missing", ex.getMessage()); } } @@ -188,49 +176,47 @@ void from_qcStatementRequiredButMissing() { class SignatureValidation { @Test - void from_validRawDigestSignature() { + void validate_validRawDigestSignature() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - SignatureResponse response = signatureResponseValidator.from(sessionStatus, "QUALIFIED"); + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, "QUALIFIED"); assertEquals("OK", response.getEndResult()); } @ParameterizedTest @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void from_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - SignatureResponse response = signatureResponseValidator.from(sessionStatus, certificateLevel.name()); + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel.name()); assertEquals("OK", response.getEndResult()); assertEquals("QUALIFIED", response.getCertificateLevel()); } @Test - void from_rawDigestUnexpectedAlgorithm() { + void validate_rawDigestUnexpectedAlgorithm() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test - void from_unknownSignatureProtocol() { + void validate_unknownSignatureProtocol() { SessionStatus sessionStatus = createMockSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - - assertEquals("Unknown signature protocol: UNKNOWN_PROTOCOL", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); } @ParameterizedTest @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void from_handleSessionEndResultErrors(String endResult, Class expectedException) { + void validate_handleSessionEndResultErrors(String endResult, Class expectedException) { var sessionResult = new SessionResult(); sessionResult.setEndResult(endResult); @@ -238,12 +224,12 @@ void from_handleSessionEndResultErrors(String endResult, Class signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); } @ParameterizedTest @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { var sessionResultDetails = new SessionResultDetails(); sessionResultDetails.setInteraction(interaction); @@ -255,219 +241,255 @@ void from_endResultIsUserRefusedInteraction(String interaction, Class signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); } @Test - void from_endResultMissing_throwsException() { + void validate_endResultMissing_throwsException() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); sessionStatus.getResult().setEndResult(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("End result parameter is missing in the session result", ex.getMessage()); + assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); } @Test - void from_sessionStatusNull() { - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.from(null, "QUALIFIED")); - assertEquals("Session status was not provided", ex.getMessage()); + void validate_sessionStatusNull() { + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, "QUALIFIED")); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); } @Test - void from_signatureMissing() { + void validate_signatureMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignature(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature object is missing", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); } @Test - void from_signatureValueMissing() { + void validate_signatureValueMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setValue(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature value is missing or not Base64", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); } @Test - void from_signatureAlgorithmMissing() { + void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue("invalid-not+encoded+value"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setSignatureAlgorithm(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Signature algorithm is missing", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test - void from_flowTypeMissing() { + void validate_flowTypeMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setFlowType(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Session status field `signature.flowType` is empty", ex.getMessage()); + assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); } @Test - void from_invalidFlowType() { + void validate_invalidFlowType() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Invalid `signature.flowType` in session status", ex.getMessage()); + assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); } @Test - void from_signatureAlgorithmNotSupported() { + void validate_signatureAlgorithmNotSupported() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertTrue(ex.getMessage().contains("Unexpected signature algorithm")); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test - void from_signatureAlgorithmNotRsassaPss() { + void validate_signatureAlgorithmNotRsassaPss() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); sessionStatus.getSignature().setSignatureAlgorithm("rsa"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Unexpected signature algorithm. Expected one of: [rsassa-pss], but got: rsa", ex.getMessage()); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } - @Test - void from_signatureAlgorithmParametersHashAlgorithmMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(null); + @Nested + class SignatureAlgorithmParametersValidations { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + @Test + void validate_signatureAlgorithmParametersMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithmParameters(null); - assertEquals("Session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); + } - @Test - void from_signatureAlgorithmParametersInvalidHashAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmMissing(String hashAlgorithm) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Invalid 'signature.signatureAlgorithmParameters.hashAlgorithm' in session status", ex.getMessage()); - } + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); + } - @Test - void from_signatureAlgorithmParametersMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithmParameters(null); + @Test + void validate_invalidHashAlgorithm() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("SignatureAlgorithmParameters is missing", ex.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - @Test - void from_invalidMaskGenAlgorithmName() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' in session status", ex.getMessage()); - } + @Test + void validate_maskGenAlgorithmIsMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); - @Test - void from_signatureAlgorithmParametersMaskGenAlgorithmMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); + } - assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); - } + @ParameterizedTest + @NullAndEmptySource + void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); - @Test - void from_signatureAlgorithmParametersMaskGenAlgorithmAlgorithmEmpty() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(null); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + @Test + void validate_invalidMaskGenAlgorithmName() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); - assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); + } - @Test - void from_signatureAlgorithmParametersMaskGenHashAlgorithmEmpty() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm(null); + @Test + void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .setParameters(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); + } - assertEquals("Session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' is empty", ex.getMessage()); - } + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm(hashAlgorithm); - @Test - void from_signatureAlgorithmParametersMaskGenHashAlgorithmInvalid() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm("INVALID-HASH"); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); + } - assertEquals("Invalid 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status", ex.getMessage()); - } + @Test + void validate_maskGenHashAlgorithmInvalid() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm("INVALID-HASH"); - @Test - void from_mismatchedHashAlgorithms() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' in session status does not match 'signature.signatureAlgorithmParameters.hashAlgorithm'", ex.getMessage()); - } + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } - @Test - void from_signatureAlgorithmParametersSaltLengthMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); + @Test + void validate_mismatchedHashAlgorithms() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); + } - assertEquals("Session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); - } - @Test - void from_invalidSaltLength() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); + @Test + void validate_saltLengthIsMissing() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Invalid 'signature.signatureAlgorithmParameters.saltLength' in session status", ex.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - @ParameterizedTest - @NullAndEmptySource - void from_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); + @Test + void validate_invalidSaltLength() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); - assertEquals("Session status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); + } - @Test - void from_invalidTrailerField() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); + @ParameterizedTest + @NullAndEmptySource + void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); + } + + @Test + void validate_invalidTrailerField() { + SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.from(sessionStatus, "QUALIFIED")); - assertEquals("Invalid `signature.signatureAlgorithmParameters.trailerField` value in session status", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); + } } } diff --git a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java new file mode 100644 index 00000000..49f900a2 --- /dev/null +++ b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java @@ -0,0 +1,121 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class SignatureValueValidatorImplTest { + + // TODO - 22.08.25: replace these values when test accounts are available + private static final String CERT = "MIIHSjCCBtCgAwIBAgIQBQHi3vqqZg+tDaGzQeB2GzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUwNzI5MDgxMTAzWhcNMjgwNzI4MDgxMTAyWjBfMQswCQYDVQQGEwJFRTEUMBIGA1UEAwwLTVVTRVIsVVJNQVMxDjAMBgNVBAQMBU1VU0VSMQ4wDAYDVQQqDAVVUk1BUzEaMBgGA1UEBRMRUE5PRUUtMzkwMDMwMTI3OTgwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCf8qQkO51SM/Gdw63LObpk4kwutMSqW345PU4HC+HqQ2H03fTludjY7iBCgEWmXQjoTt6vQgDGPfBlydjZiu2GUSCL/f2DTv76BuWzR/Jw6q4+R86GRhlMJFqfqE2gqCIddVbUx+qYZ37qCddqgIoRYejdrUeWopp2xzya5gt41FM9By95e3pS/1tug7aAlPoT3Tg18+13qqru1SDGxYW+0NVojesYX3Pzz8Exz2dWcFWwMqoU3SMlAULHDC9OPMtuZBSZA2tvyuD+CHHsU13LI46iDRU2j9BVr9EBuO/uvL3U5eIkX0gpy5bdo/TWmXDijTb5udXO9cz+GMaCQTx4yuBTnC31pHw/qrEp00FRZy7yiG0expv7w4c0YiziMFK8GfhnPmNAVEyjTWImmckK9SiIZH0F/oU1VZvtX3aXsmoTzEwpzAy3KPiKxJ0ZSSsVHV+G1nZvx/1mRxKcT+rOzNcx7iY9uAzin9ajPLYTukWsGVOTgQ2GxpYrEhuf8PvQlZ62BVIvfS5swhlwXzMU8oEAsHCpUVDNCLtckkKBgoy9pYZyKbXUtUP1TTEL3ZC9/4h3Udmao6JNWp5niyHDWVpF6r56O/ORZGx1GlT1P+G9rK6bBteptvNWillGPMA5E1fdwSci7/eH8amSED0CAy0rlq+0CdMdnpasqyG5oDmYJncWhhEozQ2fI7SkvNgSiMxDnJXhi8/Zvh4j+29eC7fqG5ZsLxQ1YqaK8XsIsNJ2Lxj0BhrEgU7Zz5lILUdOILEfU1S2Wi4Ow1P23dAP/O+o6u4SDSKSM2+C5s9daq/5zJ2w2s/B8JB8Mat5MPJuzKrvSnYMIUzQjtlsuMBRIRbHmHtCjDXufF11BOCLfPUYU5GDvk6MY51+p/hZrAowQHWZYI+271UxJR9I1dCTNvo1HsiNEnLSgdOikWvmykqiDVWPe6SiRpVKBQ7MkhgvF/CrHGG0S4GBuG6E2OHEMKl73CWuqU8MrPSOQvaXY7f99ZGK9RL1OG8oxRJpJNECAwEAAaOCAo8wggKLMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTM5MDAzMDEyNzk4LUZGTDgtUTB5BgNVHSAEcjBwMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCQYHBACL7EABAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5OTAwMzAxMTIwMDAwWjCBrgYIKwYBBQUHAQMEgaEwgZ4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwCAYGBACORgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJlbjA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUq5xLZIjeh1p1kreds8ie7OgpfmwwDgYDVR0PAQH/BAQDAgZAMAoGCCqGSM49BAMDA2gAMGUCMQCdrnNqlxbO/N6FELvGd4MHeNjTIpdDSj+6Htu6W7KRFleQGe8zhK9yA2l/zSerZvwCMGgbT0nvtgyoXBhSsUhY3RWTMiee4nKn7aBKqcmrDuHC9I9o67WpttfSE4srvL+qWQ=="; + private static final byte[] PAYLOAD = Base64.getDecoder().decode(("PGRzOlNpZ25lZEluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q" + "+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGE1MTIiPjwvZHM6U2lnbmF0dXJlTWV0aG9kPjxkczpSZWZlcmVuY2UgSWQ9InItaWQtNzcwMDA4OTNlNWU1YmVjOGMwY2IyOThjNmFkMGY0YTQtMSIgVVJJPSJkdW1teS5wZGYiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5QZmVkTkt1OHFaTUk1NXk1UkdIQmlUV0NZRTFvTXBwQi9VdnNHSVhtcmJRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PGRzOlJlZmVyZW5jZSBUeXBlPSJodHRwOi8vdXJpLmV0c2kub3JnLzAxOTAzI1NpZ25lZFByb3BlcnRpZXMiIFVSST0iI3hhZGVzLWlkLTc3MDAwODkzZTVlNWJlYzhjMGNiMjk4YzZhZDBmNGE0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPjFZd014blRUYmwwZXB5S0g0OEZ0WXFDb3pNbzAxem03NWpwV1pWNDJJNlk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+").getBytes(StandardCharsets.UTF_8)); + private static final byte[] SIGNATURE_VALUE = Base64.getDecoder().decode("UEVKOrz3Mr+qAXyOjGEt3Nnb8andzicBcEdbb4T2qVyGUslHdeJfgkXccPqBnjmEbL7xoU7eHVkO02K+XNseVY5UBHnXDlMBj1TyCnfelfiZFpAppgWmHKBXC11yE1OhtQ5/siaokPGBX1nZM2rzGlHYWxXYZrOGHCrm7gQEbClL342N6bEzeVVVPnxnxDEkzpNMFvY8UIL3C55WPPNKLBzFwSfduNcLaBiHc4ghaIiJebQc1h+Kad5OAYeu35v70k4HVePbDDp1Cb7RXfMRyUx7GNFSTZiKrG16XO8krp+d9T10SGRbZNoTzxvXBjtb8SjXM6Zvx0tiNdVnsWBrEylGzjS2DVU2+MDbek9QxlxqUU5E5H/WrelywgGTEzfZekowjofQjkYXaEAvNTK8x8Me1wIJThZwfrOy6H8MyPAdgAwl7fDwsZG6QhRCeG+9VY4CtmcII6YMZccCFCy9X3SJvXga4OcSrPi+Nwh3tfvJ5pkYvLliVKSCDpslTZk7JQYcQsJ1DVefMW6BfA+V3iX35mG/VHPo789BpzlZL6Ebs/dxNSEnyyWTDECFl2k2/B38w9jO4OuFLLg/U0AvM6ZLNlLWUjsKKg1s4U+SGlLc7r3hxaWCCwx4/NP2h8f3MTquxOCt+7WrjvCNOQ33bKcFGjYyCWpfGAfVgfMenp4oa40A1+Or+Px4Sd5yD3ZTnPSMYh2UzFZOiejGAS/koBYhn60P5PKRpEkC0nq+WQJD58soelH1EKifLoBtYNzhNOAuOfGRI5nEsW94TZ8hbC/mIEBmMnhKr9Lq/+glxbqskwOavWIF5xeWTKeSt2ErvgtNxX3hTlGxdNavwPi+/qtLikrNoirE26t1WFyPMaeH6hm0rIW42h5c0IvsXrQ4258uJzpZPe/RLbjdy62wi1S35PmowFUFImlHDKSIj4plEVXkrApZDV+/bL0VR6PNr7bsIZqgamL9OyLm6vTunP+A7Q+zpaZxuun17SC1QsthiGGBk03uf4CpNVVUpsO3".getBytes(StandardCharsets.UTF_8)); + + private SignatureValueValidator signatureValueValidator; + + @BeforeEach + void setUp() { + signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + } + + @Test + void validate() throws CertificateException { + X509Certificate certificate = CertificateUtil.getX509Certificate(CERT); + RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); + + assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); + } + + @ParameterizedTest + @ArgumentsSource(EmptyInputArgumentProvider.class) + void validate_InputParametersNotProvided_throwException(byte[] signatureValue, byte[] payload, X509Certificate certificate, RsaSsaPssParameters rsaSsaPssParameters) { + assertThrows(SmartIdClientException.class, () -> signatureValueValidator.validate(signatureValue, payload, certificate, rsaSsaPssParameters)); + } + + @Test + void validateSignatureValue_IsInvalid_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + "invalidValue".getBytes(StandardCharsets.UTF_8), + PAYLOAD, + CertificateUtil.getX509Certificate(CERT), + toRsaSsaPssParameters())); + assertEquals("Signature value validation failed", ex.getMessage()); + } + + @Test + void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + SIGNATURE_VALUE, + "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), + CertificateUtil.getX509Certificate(CERT), + toRsaSsaPssParameters())); + assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); + } + + private static RsaSsaPssParameters toRsaSsaPssParameters() { + RsaSsaPssParameters rsaSsaPssParameters = new RsaSsaPssParameters(); + rsaSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); + rsaSsaPssParameters.setTrailerField(TrailerField.OXBC); + return rsaSsaPssParameters; + } + + private static class EmptyInputArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) throws CertificateException { + return Stream.of( + Arguments.of(null, null, null, null), + Arguments.of(new byte[0], null, null, null), + Arguments.of(new byte[0], new byte[0], null, null), + Arguments.of(new byte[0], new byte[0], CertificateUtil.getX509Certificate(CERT), null) + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 3c428d02..9651b0ee 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -59,6 +59,8 @@ import ee.sk.smartid.CertificateChoiceResponse; import ee.sk.smartid.CertificateChoiceResponseMapper; import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.CertificateValidator; +import ee.sk.smartid.CertificateValidatorImpl; import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.FileTrustedCAStoreBuilder; @@ -69,6 +71,8 @@ import ee.sk.smartid.SignableData; import ee.sk.smartid.SignatureResponse; import ee.sk.smartid.SignatureResponseValidator; +import ee.sk.smartid.SignatureValueValidator; +import ee.sk.smartid.SignatureValueValidatorImpl; import ee.sk.smartid.SmartIdClient; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.TrustedCACertStore; @@ -108,378 +112,403 @@ void setUp() { @Nested class DeviceLinkBasedExamples { - @Test - void anonymousAuthentication_withApp2App() { - // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Setup builder - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(rpChallenge) - .withInitialCallbackUrl("https://example.com/callback") - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") - )); - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.getSessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.getSessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); - // Will be used to calculate elapsed time being used in dynamic link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withLang("est") - .withInitialCallbackUrl("https://example.com/callback") - .buildDeviceLink(sessionSecret); - - // Use the sessionId from the authentication session response to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // The session can have different states such as RUNNING or COMPLETE. - // Check that the session has completed successfully - assertEquals("COMPLETE", sessionStatus.getState()); - - // Setup AuthenticationResponseValidator - TrustedCACertStore build = new FileTrustedCAStoreBuilder().build(); - AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(build); - // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withSemanticIdentifierAndQrCode() { - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // For security reasons a new random challenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.getSessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.getSessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); - // Will be used to calculate elapsed time being used in dynamic link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETED", sessionStatus.getState()); - - // Validate the response and return user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(trustedCaCertStore) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withDocumentNumberAndQrCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // For security reasons a new random challenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication session status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get AuthenticationSessionRequest after the request is made and store for validations - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - String sessionId = authenticationSessionResponse.getSessionID(); - // SessionID is used to query sessions status later - - String sessionToken = authenticationSessionResponse.getSessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); - - // Generate the base (unprotected) device link URI, which does not yet include the authCode - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETE", sessionStatus.getState()); - - // Validate the certificate and signature, then map the authentication response to the user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(trustedCaCertStore) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Test - void signature_withDocumentNumberAndQRCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA256); - - // Build the dynamic link signature request - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withHashAlgorithm(HashAlgorithm.SHA_256) - .withInteractions(List.of( - DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .initSignatureSession(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.getSessionID(); - String sessionToken = signatureSessionResponse.getSessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.getSessionSecret(); - Instant receivedAt = signatureSessionResponse.getReceivedAt(); - URI deviceLinkBase = signatureSessionResponse.getDeviceLinkBase(); - - // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); - SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); + @Nested + class Authentication { + + @Test + void anonymousAuthentication_withApp2App() { + // For security reasons a new hash value must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Setup builder + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withInitialCallbackUrl("https://example.com/callback") + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") + )); + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.getSessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + // Will be used to calculate elapsed time being used in dynamic link and in authCode + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withLang("est") + .withInitialCallbackUrl("https://example.com/callback") + .buildDeviceLink(sessionSecret); + + // Use the sessionId from the authentication session response to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // The session can have different states such as RUNNING or COMPLETE. + // Check that the session has completed successfully + assertEquals("COMPLETE", sessionStatus.getState()); + + // Setup AuthenticationResponseValidator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationResponseValidator authenticationResponseValidator = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifierAndQrCode() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new random challenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.getSessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + // Will be used to calculate elapsed time being used in dynamic link and in authCode + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", sessionStatus.getState()); + + // Validate the response and return user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withDocumentNumberAndQrCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new random challenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication session status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPIN("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get AuthenticationSessionRequest after the request is made and store for validations + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + String sessionId = authenticationSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.getSessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + + // Generate the base (unprotected) device link URI, which does not yet include the authCode + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETE", sessionStatus.getState()); + + // Validate the certificate and signature, then map the authentication response to the user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } } - @Test - void signature_withSemanticIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA512); - - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // Build the dynamic link signature request - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .initSignatureSession(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.getSessionID(); - String sessionToken = signatureSessionResponse.getSessionToken(); - - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.getSessionSecret(); - Instant receivedAt = signatureSessionResponse.getReceivedAt(); - - // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Display QR-code to the user - - // Get the session status poller - poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); - SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); + @Nested + class Signature { + + @Test + void signature_withDocumentNumberAndQRCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA256); + + // Build the dynamic link signature request + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withHashAlgorithm(HashAlgorithm.SHA_256) + .withInteractions(List.of( + DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.getSessionID(); + String sessionToken = signatureSessionResponse.getSessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.getSessionSecret(); + Instant receivedAt = signatureSessionResponse.getReceivedAt(); + URI deviceLinkBase = signatureSessionResponse.getDeviceLinkBase(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + // Validate signature response + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + // Validate signature value + SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticIdentifier) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes()); + signableData.setHashType(HashType.SHA512); + + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Build the dynamic link signature request + DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .initSignatureSession(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.getSessionID(); + String sessionToken = signatureSessionResponse.getSessionToken(); + + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.getSessionSecret(); + Instant receivedAt = signatureSessionResponse.getReceivedAt(); + + // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Display QR-code to the user + + // Get the session status poller + poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + // Validate signature value + SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certificateChoiceResponse.getCertificate(), + signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } } } @@ -521,7 +550,9 @@ void authentication_withDocumentNumber() { assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); // validate the sessions status and return user's identity - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(new FileTrustedCAStoreBuilder().build()) + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: authentication request will be fixed with notification-based authentication changes assertEquals("40504040001", authenticationIdentity.getIdentityCode()); @@ -569,7 +600,9 @@ void authentication_withSemanticIdentifier() { assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - AuthenticationIdentity authenticationIdentity = new AuthenticationResponseValidator(new FileTrustedCAStoreBuilder().build()) + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: will be fixed with notification-based authentication changes assertEquals("40504040001", authenticationIdentity.getIdentityCode()); @@ -671,8 +704,9 @@ void signature_withSemanticsIdentifier() { assertEquals("COMPLETE", signatureSessionStatus.getState()); TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - SignatureResponseValidator validator = new SignatureResponseValidator(trustedCaCertStore); - SignatureResponse signatureResponse = validator.from(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOEE-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); @@ -683,6 +717,28 @@ void signature_withSemanticsIdentifier() { } } + @Nested + class CertificateByDocumentNumberExamples { + + @Test + void queryCertificate() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // Build the certificate by document number request and query the certificate + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // Setup the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + + // Validate the certificate + certificateValidator.validate(certResponse.certificate()); + } + } + private static KeyStore getKeystore() { try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 5aa3833a..6b615d85 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -168,16 +168,20 @@ class Signature { @Test void initDeviceLinkSignature_withSemanticIdentifier() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); - - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); - signatureProtocolParameters.setDigest(digest); - request.setSignatureProtocolParameters(signatureProtocolParameters); + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + var request = new SignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + null, + null + ); DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); @@ -189,16 +193,21 @@ void initDeviceLinkSignature_withSemanticIdentifier() { @Test void initDeviceLinkSignature_withDocumentNumber() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); - - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); - signatureProtocolParameters.setDigest(digest); - request.setSignatureProtocolParameters(signatureProtocolParameters); + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + var request = new SignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + null, + null + ); DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); @@ -298,18 +307,21 @@ void initNotificationCertificateChoice_withDocumentNumber() { } private static SignatureSessionRequest toSignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setCertificateLevel("QUALIFIED"); - - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(); - String digest = Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)); - signatureProtocolParameters.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signatureProtocolParameters.setDigest(digest); - request.setSignatureProtocolParameters(signatureProtocolParameters); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!")))); - return request; + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + return new SignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + null, + null + ); } } } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 07923acd..e99df274 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -787,9 +787,7 @@ void initDeviceLinkSignature_relyingPartyNoPermission() { void initDeviceLinkSignature_invalidRequest() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); - SignatureSessionRequest request = new SignatureSessionRequest(); - request.setRelyingPartyUUID(""); - request.setRelyingPartyName(""); + SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); @@ -1121,39 +1119,38 @@ private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberR } private static SignatureSessionRequest createSignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID("de305d54-75b4-431b-adb2-eb6b9e546014"); - request.setRelyingPartyName("BANK123"); - - var protocolParameters = new RawDigestSignatureProtocolParameters(); - protocolParameters.setDigest("base64-encoded-digest"); - protocolParameters.setSignatureAlgorithm("rsassa-pss"); - protocolParameters.setSignatureAlgorithm("SHA3-512"); - - request.setSignatureProtocolParameters(protocolParameters); - - DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Sign the document"); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); + var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA3-512")); - return request; + return new SignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", + "BANK123", + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + null, + null, + InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign the document"))), + null, + null); } private static SignatureSessionRequest createNotificationSignatureSessionRequest() { - var request = new SignatureSessionRequest(); - request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - request.setRelyingPartyName("DEMO"); - - var protocolParameters = new RawDigestSignatureProtocolParameters(); - protocolParameters.setDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="); - protocolParameters.setSignatureAlgorithm("rsassa-pss"); - protocolParameters.setSignatureAlgorithm("sha512WithRSAEncryption"); - - request.setSignatureProtocolParameters(protocolParameters); - - DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPIN("Verify the code"); - request.setInteractions(InteractionUtil.encodeInteractionsAsBase64(List.of(interaction))); - - return request; + var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA-512")); + var interaction = DeviceLinkInteraction.displayTextAndPIN("Verify the code"); + return new SignatureSessionRequest("00000000-0000-0000-0000-000000000000", + "DEMO", + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + null, + null, + InteractionUtil.encodeInteractionsAsBase64(List.of(interaction)), + null, + null + ); } private static void assertResponseValues(DeviceLinkSessionResponse response, From 0f08b977af5c09c063de9b14208610942dabcb60 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 27 Aug 2025 18:09:09 +0300 Subject: [PATCH 33/57] Add validation for invalid certificate level value in authentication session status --- .../smartid/AuthenticationCertificateLevel.java | 13 +++++++++++++ .../AuthenticationResponseMapperImpl.java | 4 ++++ .../AuthenticationResponseMapperImplTest.java | 17 +++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java index f2606ec2..ca008201 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -26,6 +26,8 @@ * #L% */ +import java.util.Arrays; + public enum AuthenticationCertificateLevel { ADVANCED(1), QUALIFIED(2); @@ -45,4 +47,15 @@ public enum AuthenticationCertificateLevel { public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { return this == certificateLevel || this.level > certificateLevel.level; } + + /** + * Check if the given certificate level is supported + * + * @param certificateLevel the level of the certificate + * @return true if the level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel) { + return Arrays.stream(AuthenticationCertificateLevel.values()) + .anyMatch(cl -> cl.name().equals(certificateLevel)); + } } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 4d5f04da..e3c3bda9 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -271,6 +271,10 @@ private static void validateCertificate(SessionCertificate sessionCertificate) { if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); } + if (!AuthenticationCertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Authentication session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' has unsupported value"); + } } private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index 97996627..b3cb9bfc 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -756,6 +756,23 @@ void from_certificateIsInvalid_throwException() { var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); } + + @Test + void from_certificateLevelIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.certificateLevel' has unsupported value", exception.getMessage()); + } } @ParameterizedTest From 4db44f1e736d45c0287dc52cdb5f76c3e1429eb5 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 27 Aug 2025 18:11:24 +0300 Subject: [PATCH 34/57] Improvements to certificate choice session request builder (#126) * SLIB-106 - improve signature session status response validation exception messages * SLIB-106 - move common certificate validations to CertificateValidator; update Readme * SLIB-101 - fix device link certificate choice path; improve exceptions messages in the builder --- ...ertificateChoiceSessionRequestBuilder.java | 35 ++++++---------- .../sk/smartid/rest/SmartIdRestConnector.java | 4 +- ...ficateChoiceSessionRequestBuilderTest.java | 40 ++++++++----------- .../SignatureResponseValidatorTest.java | 4 -- .../java/ee/sk/smartid/SmartIdClientTest.java | 10 ++--- .../integration/ReadmeIntegrationTest.java | 2 +- .../rest/SmartIdRestConnectorTest.java | 4 +- 7 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index 4dcc2240..74b15db0 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -30,11 +30,8 @@ import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; @@ -43,10 +40,10 @@ public class DeviceLinkCertificateChoiceSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(DeviceLinkCertificateChoiceSessionRequestBuilder.class); private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; private final SmartIdConnector connector; + private String relyingPartyUUID; private String relyingPartyName; private CertificateLevel certificateLevel; @@ -149,7 +146,7 @@ public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(S *

    * * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. - * @throws SmartIdClientException if the response is invalid or missing necessary session data. + * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. * @throws UnprocessableSmartIdResponseException if the response is missing required fields. */ public DeviceLinkSessionResponse initCertificateChoice() { @@ -162,15 +159,13 @@ public DeviceLinkSessionResponse initCertificateChoice() { private void validateRequestParameters() { if (isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } if (isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } - if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { - throw new SmartIdClientException("Nonce must be between 1 and 30 characters"); + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must have length between 1 and 30 characters"); } validateInitialCallbackUrl(); } @@ -198,29 +193,25 @@ private CertificateChoiceSessionRequest createCertificateRequest() { private void validateInitialCallbackUrl() { if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("initialCallbackUrl must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } - private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { + private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionToken())) { - logger.error("Session token is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session token is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); } if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionSecret())) { - logger.error("Session secret is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session secret is missing from the response"); + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); } if (deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase() == null || deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase().toString().isBlank()) { - logger.error("deviceLinkBase is missing or empty in the response"); - throw new UnprocessableSmartIdResponseException("deviceLinkBase is missing or empty in the response"); + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); } } } diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index d3244db6..4b31366f 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -78,7 +78,7 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); private static final String SESSION_STATUS_URI = "/session/{sessionId}"; - private static final String CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "/certificatechoice/device-link/anonymous"; + private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; @@ -184,7 +184,7 @@ public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoi logger.debug("Initiating device link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) - .path(CERTIFICATE_CHOICE_DEVICE_LINK_PATH) + .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) .build(); return postDeviceLinkCertificateChoiceRequest(uri, request); diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index ec6f9da6..3b4656c0 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -46,6 +46,7 @@ import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; @@ -159,7 +160,7 @@ void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Session ID is missing from the response", ex.getMessage()); + assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -174,7 +175,7 @@ void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Session token is missing from the response", ex.getMessage()); + assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -189,7 +190,7 @@ void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecr when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Session secret is missing from the response", ex.getMessage()); + assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); } @ParameterizedTest @@ -204,7 +205,7 @@ void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("deviceLinkBase is missing or empty in the response", ex.getMessage()); + assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); } @Test @@ -220,7 +221,7 @@ void initiateCertificateChoice_missingRelyingPartyUUID() { builderService.withRelyingPartyUUID(null); var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } @Test @@ -228,23 +229,16 @@ void initiateCertificateChoice_missingRelyingPartyName() { builderService.withRelyingPartyName(null); var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } - @Test - void initiateCertificateChoice_invalidNonce() { - builderService.withNonce("1234567890123456789012345678901"); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_emptyNonce() { - builderService.withNonce(""); + @ParameterizedTest + @ValueSource(strings = {"","1234567890123456789012345678901" }) + void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { + builderService.withNonce(invalidNonce); var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Nonce must be between 1 and 30 characters", ex.getMessage()); + assertEquals("Value for 'nonce' must have length between 1 and 30 characters", ex.getMessage()); } @Test @@ -271,7 +265,7 @@ void initiateCertificateChoice_nullNonce() { @ParameterizedTest @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url) { var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -279,7 +273,7 @@ void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url .withInitialCallbackUrl(url); var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); - assertEquals(expectedErrorMessage, exception.getMessage()); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); } } @@ -296,9 +290,9 @@ private static class InvalidInitialCallbackUrlArgumentProvider implements Argume @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("http://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "initialCallbackUrl must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") ); } } diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index 2f23db3c..c3f03268 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -252,7 +252,6 @@ void validate_endResultMissing_throwsException() { sessionStatus.getResult().setEndResult(null); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); } @@ -451,14 +450,12 @@ void validate_mismatchedHashAlgorithms() { assertEquals("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); } - @Test void validate_saltLengthIsMissing() { SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); } @@ -478,7 +475,6 @@ void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailer sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 88b7975a..362eec2e 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -78,7 +78,7 @@ class DeviceLinkCertificateChoiceSession { @Test void createDeviceLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") @@ -425,7 +425,7 @@ void createDynamicContent_authenticationWithQRCode() { @Test void createDynamicContent_certificateChoiceWithWithQRCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -449,8 +449,8 @@ void createDynamicContent_certificateChoiceWithWithQRCode() { @ParameterizedTest @EnumSource(value = DeviceLinkType.class, names = { "WEB_2_APP", "APP_2_APP" }) - void createDynamicContent_certificateChoiceWithWithWeb2AppAndApp2App(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -472,7 +472,7 @@ void createDynamicContent_certificateChoiceWithWithWeb2AppAndApp2App(DeviceLinkT @Test void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 9651b0ee..2390fbe4 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -170,7 +170,7 @@ void anonymousAuthentication_withApp2App() { // Check that the session has completed successfully assertEquals("COMPLETE", sessionStatus.getState()); - // Setup AuthenticationResponseValidator + // Set up AuthenticationResponseValidator TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); AuthenticationResponseValidator authenticationResponseValidator = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index e99df274..f0c60bfa 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -522,7 +522,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { @WireMockTest(httpPort = 18089) class DeviceLinkCertificateChoiceTests { - private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/certificatechoice/device-link/anonymous"; + private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/signature/certificate-choice/device-link/anonymous"; private SmartIdConnector connector; @@ -588,7 +588,7 @@ void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationExcep Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Request is unauthorized for URI http://localhost:18089/certificatechoice/device-link/anonymous", exception.getMessage()); + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); } @Test From 99114dd6dbae30dd5e1a63dcd2059515ba2182d5 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 28 Aug 2025 13:08:01 +0300 Subject: [PATCH 35/57] Add validations for invalid values in queried certificate state and level (#129) * SLIB-130 - add validations for invalid values in queried certificate state and level * SLIB-130 - fix get certificate by document number invalid state test --- ...ificateByDocumentNumberRequestBuilder.java | 70 ++++----- .../java/ee/sk/smartid/CertificateLevel.java | 15 +- .../java/ee/sk/smartid/CertificateState.java | 19 ++- ...ateByDocumentNumberRequestBuilderTest.java | 144 +++++++++++------- .../java/ee/sk/smartid/SmartIdClientTest.java | 10 +- 5 files changed, 151 insertions(+), 107 deletions(-) diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java index 0af5bfb6..ce57ae87 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,8 +26,6 @@ * #L% */ -import static ee.sk.smartid.util.StringUtil.isEmpty; - import java.util.regex.Pattern; import org.slf4j.Logger; @@ -111,9 +109,9 @@ public CertificateByDocumentNumberRequestBuilder withCertificateLevel(Certificat * Builds the request and retrieves the certificate by document number. * * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate - * @throws SmartIdClientException if any required parameters are missing or invalid + * @throws SmartIdClientException if any required parameters are missing or invalid * @throws UnprocessableSmartIdResponseException if the response is not valid - * @throws DocumentUnusableException if the document is unusable + * @throws DocumentUnusableException if the document is unusable */ public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { validateRequestParameters(); @@ -144,55 +142,49 @@ private void validateRequestParameters() { private void validateResponseParameters(CertificateResponse certificateResponse) { if (certificateResponse == null) { - logger.error("CertificateByDocumentNumberResponse is null"); throw new UnprocessableSmartIdResponseException("Certificate certificateByDocumentNumberResponse is null"); } - handleResponseState(certificateResponse.getState()); - validateCertificateLevel(certificateResponse.getCert().getCertificateLevel()); + validateState(certificateResponse); + + if (certificateResponse.getCert() == null) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); + } + validateCertificateLevel(certificateResponse); - if (certificateResponse.getCert() == null || isEmpty(certificateResponse.getCert().getValue())) { - logger.error("Parameter cert.value is missing"); + if (StringUtil.isEmpty(certificateResponse.getCert().getValue())) { throw new UnprocessableSmartIdResponseException("Parameter cert.value is missing"); } if (!BASE64_PATTERN.matcher(certificateResponse.getCert().getValue()).matches()) { - logger.error("Parameter cert.value is not a valid Base64-encoded string"); throw new UnprocessableSmartIdResponseException("Parameter cert.value is not a valid Base64-encoded string"); } } - private void validateCertificateLevel(String certificateLevel) { - if (StringUtil.isEmpty(certificateLevel)) { - logger.error("Parameter certificateLevel is missing"); - throw new UnprocessableSmartIdResponseException("Parameter certificateLevel is missing"); + private static void validateState(CertificateResponse certificateResponse) { + String state = certificateResponse.getState(); + if (StringUtil.isEmpty(state)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); } - - try { - CertificateLevel level = CertificateLevel.valueOf(certificateLevel); - if (!level.isSameLevelOrHigher(this.certificateLevel)) { - logger.error("Certificate level is lower than requested"); - throw new UnprocessableSmartIdResponseException("Certificate level is lower than requested"); - } - } catch (IllegalArgumentException e) { - logger.error("Invalid certificateLevel: {}", certificateLevel); - throw new UnprocessableSmartIdResponseException("Invalid certificateLevel: " + certificateLevel); + if (!CertificateState.isSupported(state)) { + logger.error("Queried certificate response field 'state' has invalid value: {}", state); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' has unsupported value"); + } + if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { + throw new DocumentUnusableException(); } } - private void handleResponseState(String state) { - if (isEmpty(state)) { - logger.error("Response state is missing"); - throw new UnprocessableSmartIdResponseException("Missing response 'state'"); + private void validateCertificateLevel(CertificateResponse certificateResponse) { + String certificateLevel = certificateResponse.getCert().getCertificateLevel(); + if (StringUtil.isEmpty(certificateLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); } - - try { - if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { - logger.error("Document is unusable"); - throw new DocumentUnusableException(); - } - } catch (IllegalArgumentException e) { - logger.error("Unsupported certificate state: {}", state); - throw new UnprocessableSmartIdResponseException("Unsupported certificate state: " + state); + if (!CertificateLevel.isSupported(certificateLevel)) { + logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); + } + if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(this.certificateLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); } } } diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index d4a8684c..8d750e79 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -26,6 +26,8 @@ * #L% */ +import java.util.Arrays; + public enum CertificateLevel { ADVANCED(1), QUALIFIED(2), @@ -41,9 +43,20 @@ public enum CertificateLevel { * Check if current certificate level is same or higher than the given certificate level * * @param certificateLevel the level of the certificate - * @return the level of the certificate + * @return true if the current level is same or higher than the given level, false otherwise */ public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { return this == certificateLevel || this.level >= certificateLevel.level; } + + /** + * Checks if the given certificate level value is supported + * + * @param certificateLevel the certificate level string to check + * @return true if the certificate level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel){ + return Arrays.stream(CertificateLevel.values()) + .anyMatch(level -> level.name().equals(certificateLevel)); + } } diff --git a/src/main/java/ee/sk/smartid/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java index b064b2ef..bf8c3015 100644 --- a/src/main/java/ee/sk/smartid/CertificateState.java +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,8 +26,21 @@ * #L% */ +import java.util.Arrays; + public enum CertificateState { OK, - DOCUMENT_UNUSABLE + DOCUMENT_UNUSABLE; + + /** + * Checks if the given certificate state value is supported + * + * @param certificateState the certificate state string to check + * @return true if the certificate state is supported, false otherwise + */ + public static boolean isSupported(String certificateState) { + return Arrays.stream(CertificateState.values()) + .anyMatch(state -> state.name().equals(certificateState)); + } } diff --git a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java index 843051ea..387415e7 100644 --- a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -36,14 +36,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateInfo; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -51,6 +43,14 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.mockito.ArgumentCaptor; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateInfo; +import ee.sk.smartid.rest.dao.CertificateResponse; + class CertificateByDocumentNumberRequestBuilderTest { private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; @@ -137,87 +137,113 @@ void getCertificate_relyingPartyNameMissing_throwException(String relyingPartyNa class ValidateRequiredResponseParameters { @Test - void getCertificate_certValueMissing_throwException() { - CertificateResponse response = createValidResponse(null, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - + void getCertificate_responseIsNull_throwException() { + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter cert.value is missing", ex.getMessage()); + assertEquals("Certificate certificateByDocumentNumberResponse is null", ex.getMessage()); } - @Test - void getCertificate_certValueInvalidBase64_throwException() { - var cert = new CertificateInfo(); - cert.setValue("NOT@BASE64!"); - cert.setCertificateLevel(CertificateLevel.QUALIFIED.name()); - - var response = new CertificateResponse(); - response.setCert(cert); - response.setState(CertificateState.OK.name()); + @Nested + class ValidateState { + + @Test + void getCertificate_responseStateMissing_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + response.setState(null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' is missing", ex.getMessage()); + } + + @Test + void getCertificate_responseStateValueIsInvalid_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + response.setState("invalid"); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); + } + + @Test + void getCertificate_responseStateIsDocumentUnusable_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + response.setState(CertificateState.DOCUMENT_UNUSABLE.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); + + assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); + } + } - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + @Nested + class ValidateCertificateLevel { - var builder = createValidRequestParameters(); + @Test + void getCertificate_responseCertificateLevelMissing_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter cert.value is not a valid Base64-encoded string", ex.getMessage()); - } + var builder = createValidRequestParameters(); - @Test - void getCertificate_responseStateIsDocumentUnusable_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - response.setState(CertificateState.DOCUMENT_UNUSABLE.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' is missing", ex.getMessage()); + } - var builder = createValidRequestParameters(); + @Test + void getCertificate_responseCertificateHasInvalidValue_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, "invalid"); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); - assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); - } + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } - @Test - void getCertificate_responseIsNull_throwException() { - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); + @Test + void getCertificate_certificateLevelLowerThanRequested_throwException() { + CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - var builder = createValidRequestParameters(); + var builder = createValidRequestParameters(); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Certificate certificateByDocumentNumberResponse is null", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate has lower level than requested", ex.getMessage()); + } } @Test - void getCertificate_responseStateMissing_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - response.setState(null); + void getCertificate_certValueMissing_throwException() { + CertificateResponse response = createValidResponse(null, CertificateLevel.QUALIFIED.name()); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Missing response 'state'", ex.getMessage()); + assertEquals("Parameter cert.value is missing", ex.getMessage()); } @Test - void getCertificate_responseCertificateLevelMissing_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); + void getCertificate_certValueInvalidBase64_throwException() { + var cert = new CertificateInfo(); + cert.setValue("NOT@BASE64!"); + cert.setCertificateLevel(CertificateLevel.QUALIFIED.name()); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter certificateLevel is missing", ex.getMessage()); - } + var response = new CertificateResponse(); + response.setCert(cert); + response.setState(CertificateState.OK.name()); - @Test - void getCertificate_certificateLevelLowerThanRequested_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Certificate level is lower than requested", ex.getMessage()); + assertEquals("Parameter cert.value is not a valid Base64-encoded string", ex.getMessage()); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 362eec2e..5535c527 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -47,14 +47,14 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.rest.dao.HashAlgorithm; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; class SmartIdClientTest { @@ -248,7 +248,7 @@ void getCertificateByDocumentNumber_withUnknownState_throwsException() { var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertTrue(ex.getMessage().contains("Unsupported certificate state")); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); } } @@ -369,7 +369,7 @@ void getSessionStatus() { class DynamicContent { @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = { "WEB_2_APP", "APP_2_APP" }) + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) void createDynamicContent_authenticationWithWeb2AppAndApp2App(DeviceLinkType deviceLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); @@ -448,7 +448,7 @@ void createDynamicContent_certificateChoiceWithWithQRCode() { } @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = { "WEB_2_APP", "APP_2_APP" }) + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); From 3934681d6213569c5a2644f7ac8e337900ac4aba Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 3 Sep 2025 10:11:01 +0300 Subject: [PATCH 36/57] Improve query certificate by document nr (#130) * SLIB-98 - update exception message of DocumentUnusableException * SLIB-98 - fix NullPointerException when certificate level is set to null * SLIB-98 - update validation exception messages for CertificateByDocumentNumberRequestBuilder * SLIB-98 - change CertificateByDocumentNumberRequest into record * SLIB-98 - change CertificateResponse and CertificateInfo into records * SLIB-98 - remove todos --- CHANGELOG.md | 5 + ...ificateByDocumentNumberRequestBuilder.java | 39 +++-- .../DocumentUnusableException.java | 5 +- .../CertificateByDocumentNumberRequest.java | 36 +---- .../sk/smartid/rest/dao/CertificateInfo.java | 21 +-- .../smartid/rest/dao/CertificateResponse.java | 25 +--- ...ateByDocumentNumberRequestBuilderTest.java | 135 ++++++++++-------- .../java/ee/sk/smartid/SmartIdClientTest.java | 4 +- .../rest/SmartIdRestConnectorTest.java | 34 +++-- ...y-document-number-request-all-fields.json} | 0 ...t-number-request-only-required-fields.json | 4 + 11 files changed, 143 insertions(+), 165 deletions(-) rename src/test/resources/requests/{certificate-by-document-number-request.json => sign/certificate-by-document-number-request-all-fields.json} (100%) create mode 100644 src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e036a53c..2a147849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. + +## [3.1.10] - 2025-08-28 +- Updated exception message of `DocumentUnusableException` + ## [3.1.9] - 2025-07-20 - Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and `SignatureResponseValidator`. diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java index ce57ae87..83580a76 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -115,53 +115,49 @@ public CertificateByDocumentNumberRequestBuilder withCertificateLevel(Certificat */ public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { validateRequestParameters(); - var request = new CertificateByDocumentNumberRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - request.setCertificateLevel(certificateLevel.name()); + var request = new CertificateByDocumentNumberRequest(relyingPartyUUID, relyingPartyName, certificateLevel == null ? null : certificateLevel.name()); CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); validateResponseParameters(response); - return new CertificateByDocumentNumberResult(CertificateLevel.valueOf(response.getCert().getCertificateLevel()), CertificateParser.parseX509Certificate(response.getCert().getValue())); + return new CertificateByDocumentNumberResult( + CertificateLevel.valueOf(response.cert().certificateLevel()), + CertificateParser.parseX509Certificate(response.cert().value())); } private void validateRequestParameters() { if (StringUtil.isEmpty(documentNumber)) { - logger.error("Parameter documentNumber must be set"); - throw new SmartIdClientException("Parameter documentNumber must be set"); + throw new SmartIdClientException("Value for 'documentNumber' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + throw new SmartIdClientException("Value for 'relyingPartyUUID' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Value for 'relyingPartyName' cannot be empty"); } } private void validateResponseParameters(CertificateResponse certificateResponse) { if (certificateResponse == null) { - throw new UnprocessableSmartIdResponseException("Certificate certificateByDocumentNumberResponse is null"); + throw new UnprocessableSmartIdResponseException("Queried certificate response is not provided"); } validateState(certificateResponse); - if (certificateResponse.getCert() == null) { + if (certificateResponse.cert() == null) { throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); } validateCertificateLevel(certificateResponse); - if (StringUtil.isEmpty(certificateResponse.getCert().getValue())) { - throw new UnprocessableSmartIdResponseException("Parameter cert.value is missing"); + if (StringUtil.isEmpty(certificateResponse.cert().value())) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' is missing"); } - - if (!BASE64_PATTERN.matcher(certificateResponse.getCert().getValue()).matches()) { - throw new UnprocessableSmartIdResponseException("Parameter cert.value is not a valid Base64-encoded string"); + if (!BASE64_PATTERN.matcher(certificateResponse.cert().value()).matches()) { + logger.error("Certificate response field 'cert.value' has invalid value: {}", certificateResponse.cert().value()); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' does not have Base64-encoded value"); } } private static void validateState(CertificateResponse certificateResponse) { - String state = certificateResponse.getState(); + String state = certificateResponse.state(); if (StringUtil.isEmpty(state)) { throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); } @@ -175,7 +171,7 @@ private static void validateState(CertificateResponse certificateResponse) { } private void validateCertificateLevel(CertificateResponse certificateResponse) { - String certificateLevel = certificateResponse.getCert().getCertificateLevel(); + String certificateLevel = certificateResponse.cert().certificateLevel(); if (StringUtil.isEmpty(certificateLevel)) { throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); } @@ -183,7 +179,8 @@ private void validateCertificateLevel(CertificateResponse certificateResponse) { logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); } - if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(this.certificateLevel)) { + CertificateLevel requestedLevel = this.certificateLevel == null ? CertificateLevel.QUALIFIED : this.certificateLevel; + if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(requestedLevel)) { throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); } } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java index dc2010cf..572e7fb9 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,7 +27,8 @@ */ public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { + public DocumentUnusableException() { - super("DOCUMENT_UNUSABLE. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); + super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java index 3028199c..929568d2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,33 +28,9 @@ import java.io.Serializable; -public class CertificateByDocumentNumberRequest implements Serializable { +import com.fasterxml.jackson.annotation.JsonInclude; - private String relyingPartyUUID; - private String relyingPartyName; - private String certificateLevel; - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } +public record CertificateByDocumentNumberRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java index ef80a451..568215e1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -31,24 +31,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public class CertificateInfo implements Serializable { - - private String value; - private String certificateLevel; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } +public record CertificateInfo(String value, String certificateLevel) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java index c758f174..0f9cf545 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,24 +31,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public class CertificateResponse implements Serializable { - - private String state; - private CertificateInfo cert; - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public CertificateInfo getCert() { - return cert; - } - - public void setCert(CertificateInfo cert) { - this.cert = cert; - } +public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { } diff --git a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java index 387415e7..3634acfa 100644 --- a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -66,11 +67,11 @@ void setUp() { } @Test - void initCertificate_ByDocumentNumber_ok() { - CertificateResponse mockResponse = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + void getCertificateByDocumentNumber_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); - var result = new CertificateByDocumentNumberRequestBuilder(connector) + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) .withDocumentNumber(DOCUMENT_NUMBER) .withRelyingPartyUUID(RP_UUID) .withRelyingPartyName(RP_NAME) @@ -88,9 +89,37 @@ void initCertificate_ByDocumentNumber_ok() { verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); CertificateByDocumentNumberRequest sentRequest = captor.getValue(); - assertEquals(RP_UUID, sentRequest.getRelyingPartyUUID()); - assertEquals(RP_NAME, sentRequest.getRelyingPartyName()); - assertEquals("QUALIFIED", sentRequest.getCertificateLevel()); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertEquals("QUALIFIED", sentRequest.certificateLevel()); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelSetToNull_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(null) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertNull(sentRequest.certificateLevel()); } @Nested @@ -98,38 +127,38 @@ class ValidateRequiredRequestParameters { @ParameterizedTest @NullAndEmptySource - void getCertificate_documentNumberMissing_throwException(String documentNumber) { + void getCertificateByDocumentNumber_documentNumberMissing_throwException(String documentNumber) { var builder = new CertificateByDocumentNumberRequestBuilder(connector) .withRelyingPartyUUID(RP_UUID) .withRelyingPartyName(RP_NAME) .withDocumentNumber(documentNumber); var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter documentNumber must be set", ex.getMessage()); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void getCertificate_relyingPartyUUIDMissing_throwException(String uuid) { + void getCertificateByDocumentNumber_relyingPartyUUIDMissing_throwException(String uuid) { var builder = new CertificateByDocumentNumberRequestBuilder(connector) .withDocumentNumber(DOCUMENT_NUMBER) .withRelyingPartyName(RP_NAME) .withRelyingPartyUUID(uuid); var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter relyingPartyUUID must be set", ex.getMessage()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void getCertificate_relyingPartyNameMissing_throwException(String relyingPartyName) { + void getCertificateByDocumentNumber_relyingPartyNameMissing_throwException(String relyingPartyName) { var builder = new CertificateByDocumentNumberRequestBuilder(connector) .withDocumentNumber(DOCUMENT_NUMBER) .withRelyingPartyUUID(RP_UUID) .withRelyingPartyName(relyingPartyName); var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } } @@ -137,22 +166,21 @@ void getCertificate_relyingPartyNameMissing_throwException(String relyingPartyNa class ValidateRequiredResponseParameters { @Test - void getCertificate_responseIsNull_throwException() { + void getCertificateByDocumentNumber_responseIsNull_throwException() { when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Certificate certificateByDocumentNumberResponse is null", ex.getMessage()); + assertEquals("Queried certificate response is not provided", ex.getMessage()); } @Nested class ValidateState { @Test - void getCertificate_responseStateMissing_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - response.setState(null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + void getCertificateByDocumentNumber_responseStateMissing_throwException() { + var certificateResponse = new CertificateResponse(null, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); @@ -160,10 +188,9 @@ void getCertificate_responseStateMissing_throwException() { } @Test - void getCertificate_responseStateValueIsInvalid_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - response.setState("invalid"); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + void getCertificateByDocumentNumber_responseStateValueIsInvalid_throwException() { + var certificateResponse = new CertificateResponse("invalid", null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); @@ -171,22 +198,32 @@ void getCertificate_responseStateValueIsInvalid_throwException() { } @Test - void getCertificate_responseStateIsDocumentUnusable_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - response.setState(CertificateState.DOCUMENT_UNUSABLE.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + void getCertificateByDocumentNumber_responseStateIsDocumentUnusable_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.DOCUMENT_UNUSABLE.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); var builder = createValidRequestParameters(); assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); } } + @Test + void getCertificateByDocumentNumber_certFieldMissing_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.OK.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert' is missing", ex.getMessage()); + } + @Nested class ValidateCertificateLevel { @Test - void getCertificate_responseCertificateLevelMissing_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, null); + void getCertificateByDocumentNumber_responseCertificateLevelMissing_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, null); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); @@ -196,8 +233,8 @@ void getCertificate_responseCertificateLevelMissing_throwException() { } @Test - void getCertificate_responseCertificateHasInvalidValue_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, "invalid"); + void getCertificateByDocumentNumber_responseCertificateHasInvalidValue_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, "invalid"); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); @@ -206,8 +243,8 @@ void getCertificate_responseCertificateHasInvalidValue_throwException() { } @Test - void getCertificate_certificateLevelLowerThanRequested_throwException() { - CertificateResponse response = createValidResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); + void getCertificateByDocumentNumber_certificateLevelLowerThanRequested_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); @@ -218,32 +255,24 @@ void getCertificate_certificateLevelLowerThanRequested_throwException() { } @Test - void getCertificate_certValueMissing_throwException() { - CertificateResponse response = createValidResponse(null, CertificateLevel.QUALIFIED.name()); + void getCertificateByDocumentNumber_certValueMissing_throwException() { + CertificateResponse response = toCertificateResponse(null, CertificateLevel.QUALIFIED.name()); when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter cert.value is missing", ex.getMessage()); + assertEquals("Queried certificate response field 'cert.value' is missing", ex.getMessage()); } @Test - void getCertificate_certValueInvalidBase64_throwException() { - var cert = new CertificateInfo(); - cert.setValue("NOT@BASE64!"); - cert.setCertificateLevel(CertificateLevel.QUALIFIED.name()); - - var response = new CertificateResponse(); - response.setCert(cert); - response.setState(CertificateState.OK.name()); - - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - + void getCertificateByDocumentNumber_certValueInvalidBase64_throwException() { + CertificateResponse certificateResponse = toCertificateResponse("NOT@BASE64!", CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); var builder = createValidRequestParameters(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Parameter cert.value is not a valid Base64-encoded string", ex.getMessage()); + assertEquals("Queried certificate response field 'cert.value' does not have Base64-encoded value", ex.getMessage()); } } @@ -254,14 +283,8 @@ private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() .withRelyingPartyName(RP_NAME); } - private CertificateResponse createValidResponse(String certValue, String level) { - var certificate = new CertificateInfo(); - certificate.setValue(certValue); - certificate.setCertificateLevel(level); - - var response = new CertificateResponse(); - response.setCert(certificate); - response.setState(CertificateState.OK.name()); - return response; + private CertificateResponse toCertificateResponse(String certValue, String level) { + var certificate = new CertificateInfo(certValue, level); + return new CertificateResponse(CertificateState.OK.name(), certificate); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 5535c527..91c0869d 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -226,7 +226,7 @@ class CertificateByDocumentNumberRequest { @Test void createCertificateRequest_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") @@ -240,7 +240,7 @@ void createCertificateRequest_withDocumentNumber() { @Test void getCertificateByDocumentNumber_withUnknownState_throwsException() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response-unknown-state.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response-unknown-state.json"); CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() .withDocumentNumber("PNOEE-1234567890-MOCK-Q") diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index f0c60bfa..233487ca 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -689,20 +689,34 @@ void setUp() { @Test void getCertificateByDocumentNumber_successful() { - SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json", "responses/certificate-by-document-number-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); CertificateResponse response = connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); assertNotNull(response); - assertEquals("OK", response.getState()); - assertNotNull(response.getCert()); - assertEquals("QUALIFIED", response.getCert().getCertificateLevel()); - assertThat(response.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); + + var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", null); + CertificateResponse response = connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", certificateByDocumentNumberRequest); + + assertNotNull(response); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); } @Test void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); assertThrows(UserAccountNotFoundException.class, () -> { connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); }); @@ -710,7 +724,7 @@ void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { @Test void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/certificate-by-document-number-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); assertThrows(RelyingPartyAccountConfigurationException.class, () -> { connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); }); @@ -1111,11 +1125,7 @@ private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest } private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { - var request = new CertificateByDocumentNumberRequest(); - request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - request.setRelyingPartyName("DEMO"); - request.setCertificateLevel("ADVANCED"); - return request; + return new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", "ADVANCED"); } private static SignatureSessionRequest createSignatureSessionRequest() { diff --git a/src/test/resources/requests/certificate-by-document-number-request.json b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json similarity index 100% rename from src/test/resources/requests/certificate-by-document-number-request.json rename to src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json new file mode 100644 index 00000000..834ac077 --- /dev/null +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json @@ -0,0 +1,4 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO" +} \ No newline at end of file From 6605636f85ddfbc2523065938edf8ec03a163e3d Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 4 Sep 2025 10:42:37 +0300 Subject: [PATCH 37/57] Update device link certificate choice session status validations (#127) * SLIB-106 - extract signature value validations to its own class * SLIB-102 - rename CertficateChoiceResponseMapper to CertificateChoiceValidator; update validations and exception messages * SLIB-102 - change CertificateChoiceSessionRequest and DeviceLinkSessionResponse to records * SLIB-102 - move certificate purpose validations to specific classes; improve code quality * SLIB-102 - improve tests in SmartIdClientTest; improve code style --- CHANGELOG.md | 5 + README.md | 121 ++--- .../AuthenticationResponseMapperImpl.java | 11 +- .../AuthenticationResponseValidator.java | 5 +- .../CertificateChoiceResponseMapper.java | 133 ----- .../CertificateChoiceResponseValidator.java | 168 ++++++ ...nkAuthenticationSessionRequestBuilder.java | 10 +- ...ertificateChoiceSessionRequestBuilder.java | 37 +- ...iceLinkSignatureSessionRequestBuilder.java | 8 +- ...dSignatureCertificatePurposeValidator.java | 76 +++ ...ertificateChoiceSessionRequestBuilder.java | 38 +- ...dSignatureCertificatePurposeValidator.java | 128 +++++ .../SignatureCertificatePurposeValidator.java | 42 ++ ...ureCertificatePurposeValidatorFactory.java | 38 ++ ...ertificatePurposeValidatorFactoryImpl.java | 44 ++ .../java/ee/sk/smartid/SignatureResponse.java | 12 +- .../smartid/SignatureResponseValidator.java | 128 ++--- .../smartid/SignatureValueValidatorImpl.java | 12 - .../dao/CertificateChoiceSessionRequest.java | 82 +-- .../rest/dao/DeviceLinkSessionResponse.java | 63 +-- .../util/CertificateAttributeUtil.java | 62 ++- .../AuthenticationIdentityMapperTest.java | 20 +- .../AuthenticationResponseMapperImplTest.java | 39 +- .../AuthenticationResponseValidatorTest.java | 9 +- .../CertificateChoiceResponseMapperTest.java | 284 ----------- ...ertificateChoiceResponseValidatorTest.java | 290 +++++++++++ .../java/ee/sk/smartid/CertificateUtil.java | 34 +- .../smartid/CertificateValidatorImplTest.java | 18 +- .../DefaultTrustedCAStoreBuilderTest.java | 27 +- ...thenticationSessionRequestBuilderTest.java | 29 +- ...ficateChoiceSessionRequestBuilderTest.java | 83 ++- ...inkSignatureSessionRequestBuilderTest.java | 62 +-- .../smartid/InvalidCertificateGenerator.java | 112 ++++ ...natureCertificatePurposeValidatorTest.java | 120 +++++ ...ficateChoiceSessionRequestBuilderTest.java | 12 +- ...natureCertificatePurposeValidatorTest.java | 148 ++++++ .../SignatureResponseValidatorTest.java | 293 ++++++----- .../SignatureValueValidatorImplTest.java | 14 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 480 +++++++++++++----- .../sk/smartid/SmartIdRestServiceStubs.java | 10 + .../integration/ReadmeIntegrationTest.java | 98 ++-- .../SmartIdRestIntegrationTest.java | 66 +-- .../rest/SmartIdRestConnectorTest.java | 100 ++-- .../util/CertificateAttributeUtilTest.java | 44 +- .../util/NationalIdentityNumberUtilTest.java | 8 +- ...ate-choice-session-request-all-fields.json | 10 + ...te-choice-session-request-device-link.json | 6 + ...te-choice-session-request-for-qr-code.json | 5 + ...ice-link-signature-request-all-fields.json | 14 + ...evice-link-signature-request-qr-code.json} | 0 ...ce-link-signature-request-same-device.json | 14 + .../certificate-choice-session-request.json | 0 ...otification-signature-session-request.json | 0 ...k-certificate-choice-session-response.json | 0 ...evice-link-signature-session-response.json | 2 +- .../resources/test-certs/nq-signing-cert.pem | 37 ++ src/test/resources/trusted_certificates.jks | Bin 4573 -> 5606 bytes 57 files changed, 2330 insertions(+), 1381 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java create mode 100644 src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java create mode 100644 src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java create mode 100644 src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java delete mode 100644 src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java create mode 100644 src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java create mode 100644 src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json create mode 100644 src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json create mode 100644 src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json create mode 100644 src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json rename src/test/resources/requests/{device-link-signature-request.json => sign/device-link/signature/device-link-signature-request-qr-code.json} (100%) create mode 100644 src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json rename src/test/resources/requests/{ => sign/notification}/certificate-choice-session-request.json (100%) rename src/test/resources/requests/{ => sign/notification}/notification-signature-session-request.json (100%) rename src/test/resources/responses/{ => sign/device-link/certificate-choice}/device-link-certificate-choice-session-response.json (100%) rename src/test/resources/responses/{ => sign/device-link/signature}/device-link-signature-session-response.json (85%) create mode 100644 src/test/resources/test-certs/nq-signing-cert.pem diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a147849..12ab6a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.11] - 2025-08-25 +- Updated CertificateChoiceResponseMapper + - Renamed to CertificateChoiceResponseValidator + - Added CertificateValidator as dependency + ## [3.1.10] - 2025-08-28 - Updated exception message of `DocumentUnusableException` diff --git a/README.md b/README.md index ebea79fe..1c7332b3 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/ * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. * `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters.. + * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters. * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. @@ -215,15 +215,15 @@ DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthentica AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = authenticationSessionResponse.sessionID(); // Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: // - Generate QR-code or device link to be displayed to the user @@ -264,15 +264,15 @@ DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthentica AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = authenticationSessionResponse.sessionID(); // Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: // - Generate QR-code or device link to be displayed to the user @@ -306,15 +306,15 @@ DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthentica AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = authenticationSessionResponse.sessionID(); // Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.getSessionToken(); +String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.getSessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: // - Generate QR-code or device link to be displayed to the user @@ -357,14 +357,14 @@ DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificate .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) .initiateCertificateChoice(); -String sessionId = certificateChoice.getSessionID(); +String sessionId = certificateChoice.sessionID(); // SessionID is used to query sessions status later -String sessionToken = certificateChoice.getSessionToken(); +String sessionToken = certificateChoice.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = certificateChoice.getSessionSecret(); -String deviceLinkBase = certificateChoice.getDeviceLinkBase(); -Instant responseReceivedAt = certificateChoice.getReceivedAt(); +String sessionSecret = certificateChoice.sessionSecret(); +String deviceLinkBase = certificateChoice.deviceLinkBase(); +Instant responseReceivedAt = certificateChoice.receivedAt(); ``` Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -430,12 +430,12 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .initSignatureSession(); // Process the signature response -String sessionID = signatureResponse.getSessionID(); -String sessionToken = signatureResponse.getSessionToken(); +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.getSessionSecret(); -Instant receivedAt = signatureResponse.getReceivedAt(); -String deviceLinkBase = signatureResponse.getDeviceLinkBase(); +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); // Generate QR-code or device link to be displayed to the user // Start querying sessions status @@ -463,13 +463,13 @@ DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSign .initSignatureSession(); // Process the signature response -String sessionID = signatureResponse.getSessionID(); -String sessionToken = signatureResponse.getSessionToken(); +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.getSessionSecret(); -Instant receivedAt = signatureResponse.getReceivedAt(); -String deviceLinkBase = signatureResponse.getDeviceLinkBase(); +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); // Generate QR-code or device link to be displayed to the user // Start querying sessions status @@ -582,17 +582,17 @@ Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Calculate elapsed seconds since session response -long elapsedSeconds = Duration.between(session.getReceivedAt(), Instant.now()).getSeconds(); +long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.getSessionToken()) + .withSessionToken(sessionResponse.sessionToken()) .withElapsedSeconds(elapsedSeconds) .withLang("eng") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .buildDeviceLink(sessionResponse.getSessionSecret()); + .buildDeviceLink(sessionResponse.sessionSecret()); ``` ##### Overriding default values @@ -602,14 +602,14 @@ DeviceLinkSessionResponse sessionResponse; // response from the session initiati // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() .withSchemeName("smart-id-demo") // override default scheme name to use demo environment - .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) .withDeviceLinkType(DeviceLinkType.APP_2_APP) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.getSessionToken()) + .withSessionToken(sessionResponse.sessionToken()) .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withInitialCallbackUrl("https://your-app/callback") - .buildDeviceLink(sessionResponse.getSessionSecret()); + .buildDeviceLink(sessionResponse.sessionSecret()); ``` #### Generating QR-code @@ -617,7 +617,7 @@ URI deviceLink = new DeviceLinkBuilder() Creating a QR code uses the Zxing library to generate a QR code image with device link as content. According to link size the QR-code of version 9 (53x53 modules) is used. For the QR-code to be scannable by most devices the QR code module size should be ~10px. -It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px)). +It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px). Generated QR code will have error correction level low. ##### Generate QR-code Data URI @@ -625,17 +625,17 @@ Generated QR code will have error correction level low. ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.getSessionToken()) + .withSessionToken(sessionResponse.sessionToken()) .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withElapsedSeconds(elapsedSeconds) - .buildDeviceLink(sessionResponse.getSessionSecret()); + .buildDeviceLink(sessionResponse.sessionSecret()); // Generate QR code image from device link URI String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); @@ -653,17 +653,17 @@ The width and height of 1159px produce a QR code with a module size of 19px. ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. // Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.getDeviceLinkBase()) + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.getSessionToken()) + .withSessionToken(sessionResponse.sessionToken()) .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withElapsedSeconds(elapsedSeconds) - .buildDeviceLink(sessionResponse.getSessionSecret()); + .buildDeviceLink(sessionResponse.sessionSecret()); // Create QR-code with height and width of 570px and quiet area of 2 modules. BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); @@ -705,7 +705,7 @@ The following example shows how to use the SessionStatusPoller to fetch the sess SessionsStatusPoller poller = client.getSessionsStatusPoller(); // Get sessionID from current session response -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID()); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ @@ -723,7 +723,7 @@ NB! If using this method for dynamic-link flows. Make sure the pollingSleepTimeo SessionStatusPoller poller = client.getSessionStatusPoller(); // Querying the sessions status -SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.getSessionID()); +SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.sessionID()); // Checking sessions state if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { // Session is still running and querying can be continued @@ -791,7 +791,7 @@ AuthenticationSessionRequest authenticationSessionRequest = authenticationReques // get sessions result SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.getSessionID()); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); // validate sessions state is completed if("COMPLETE".equals(sessionStatus.getState())){ @@ -802,10 +802,15 @@ if("COMPLETE".equals(sessionStatus.getState())){ #### Example of validating the certificate choice session response: +CertificateChoiceResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + ```java try { + // Set up CertificateChoiceResponseValidator with the CertificateValidator + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(sessionStatus); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(sessionStatus); + } catch (UserRefusedException e) { System.out.println("User refused the session."); } catch (SessionTimeoutException e) { @@ -824,14 +829,14 @@ SignatureResponseValidator depends on CertificateValidator. Checkout [setting up ```java try { // Objects needed for validation - CertificateResponse certResponse; // queried by document number or from certificate choice session + CertificateResponse certResponse; // queried by document number or use CertificateChoiceResponse SignableData signableData; // data that was sent for signing // Initialize the signature response validator with CertificateValidator SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); // Validate signature value. This step can be skipped if other means of validating the signature value can be used. - SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), @@ -976,7 +981,7 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client )) .initAuthenticationSession(); -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = authenticationSessionResponse.sessionID(); // SessionID is used to query sessions status later String verificationCode = authenticationSessionResponse.getVc().getValue(); @@ -1008,7 +1013,7 @@ NotificationAuthenticationSessionResponse authenticationSessionResponse = client )) .initAuthenticationSession(); -String sessionId = authenticationSessionResponse.getSessionID(); +String sessionId = authenticationSessionResponse.sessionID(); // SessionID is used to query sessions status later String verificationCode = authenticationSessionResponse.getVc().getValue(); @@ -1049,7 +1054,7 @@ NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); -String sessionId = certificateChoiceSessionResponse.getSessionID(); +String sessionId = certificateChoiceSessionResponse.sessionID(); // SessionID is used to query sessions status later ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -1109,7 +1114,7 @@ NotificationSignatureSessionResponse signatureSessionResponse = client.createNot .initSignatureSession(); // Process the querying sessions status response -String sessionID = signatureSessionResponse.getSessionID(); +String sessionID = signatureSessionResponse.sessionID(); // Display verification code to the user String verificationCode = signatureSessionResponse.getVc().getValue(); @@ -1138,7 +1143,7 @@ NotificationSignatureSessionResponse signatureResponse = client.createNotificati .initSignatureSession(); // Process the signature response -String sessionID = signatureResponse.getSessionID(); +String sessionID = signatureResponse.sessionID(); // Display verification code to the user String verificationCode = signatureResponse.getVc().getValue(); diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index e3c3bda9..e4473f29 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -48,21 +48,12 @@ public class AuthenticationResponseMapperImpl implements AuthenticationResponseM private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); - private static AuthenticationResponseMapper instance; - private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; - public static AuthenticationResponseMapper getInstance() { - if (instance == null) { - instance = new AuthenticationResponseMapperImpl(); - } - return instance; - } - /** - * Maps session status to authentication response {@link AuthenticationResponse] + * Maps session status to authentication response {@link AuthenticationResponse} * * @param sessionStatus session status received from Smart-ID server * @return authentication response diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index df8ad7f6..ee6c42d5 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -80,12 +80,13 @@ public AuthenticationResponseValidator(CertificateValidator certificateValidator * Creates an instance of {@link AuthenticationResponseValidator} using {@link CertificateValidator} * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted * @return a new instance of {@link AuthenticationResponseValidator} */ public static AuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { return new AuthenticationResponseValidator(certificateValidator, - AuthenticationResponseMapperImpl.getInstance(), - SignatureValueValidatorImpl.getInstance()); + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl()); } /** diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java deleted file mode 100644 index c2488519..00000000 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponseMapper.java +++ /dev/null @@ -1,133 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates and maps the received session status to certificate choice response - */ -public class CertificateChoiceResponseMapper { - - /** - * Maps session status to certificate choice response - *

    - * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level - * - * @param sessionStatus session status received from Smart-ID server - * @return certificate choice response - */ - public static CertificateChoiceResponse from(SessionStatus sessionStatus) { - return from(sessionStatus, CertificateLevel.QUALIFIED); - } - - /** - * Maps session status to certificate choice response - * - * @param sessionStatus session status received from Smart-ID server - * @param requestedCertificateLevel requested certificate level - * @return certificate choice response - */ - public static CertificateChoiceResponse from(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - validateSessionStatus(sessionStatus); - X509Certificate certificate = getValidatedCertificate(sessionStatus, requestedCertificateLevel); - - var certificateChoiceResponse = new CertificateChoiceResponse(); - certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); - certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); - certificateChoiceResponse.setCertificate(certificate); - certificateChoiceResponse.setCertificateLevel(CertificateLevel.valueOf(sessionStatus.getCert().getCertificateLevel())); - certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return certificateChoiceResponse; - } - - private static void validateSessionStatus(SessionStatus sessionStatus) { - if (sessionStatus == null) { - throw new SmartIdClientException("Session status parameter is not provided"); - } - validateResult(sessionStatus.getResult()); - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Session result parameter is missing"); - } - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("End result parameter is missing in the session result"); - } - if (!"OK".equalsIgnoreCase(endResult)) { - ErrorResultHandler.handle(sessionResult); - } - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Document number parameter is missing in the session result"); - } - } - - private static X509Certificate getValidatedCertificate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - X509Certificate certificate = CertificateParser.parseX509Certificate(sessionStatus.getCert().getValue()); - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - throw new UnprocessableSmartIdResponseException("Signer's certificate is not valid", ex); - } - return certificate; - } - - private static void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Certificate parameter is missing in session status"); - } - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Value parameter is missing in certificate"); - } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate level parameter is missing in certificate"); - } - if (!isCertificateLevelValid(requestedCertificateLevel.name(), sessionCertificate.getCertificateLevel())) { - throw new CertificateLevelMismatchException("Certificate level returned by Smart-ID is lower than requested"); - } - } - - private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { - CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); - CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); - return returnedLevel.isSameLevelOrHigher(requestedLevel); - } -} diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java new file mode 100644 index 00000000..c4c76613 --- /dev/null +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java @@ -0,0 +1,168 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to certificate choice response + */ +public class CertificateChoiceResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateChoiceResponseValidator.class); + + private final CertificateValidator certificateValidator; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; + + /** + * Initializes the certificate choice response validator with a certificate validator + * + * @param certificateValidator certificate validator to validate the received certificate + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Initializes the certificate choice response validator with a certificate validator and signature certificate purpose validator factory + * + * @param certificateValidator certificate validator to validate the received certificate + * @param signatureCertificatePurposeValidatorFactory factory to create signature certificate purpose validators + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; + } + + /** + * Validates certificate choice session status response + *

    + * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @return certificate choice response {@link CertificateChoiceResponse} + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus) { + return validate(sessionStatus, CertificateLevel.QUALIFIED); + } + + /** + * Validates session status to certificate choice response with the requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @param requestedCertificateLevel requested certificate level + * @return certificate choice response {@link CertificateChoiceResponse} + * @throws SmartIdClientException when the parameters are not provided + * @throws UnprocessableSmartIdResponseException when any required field is missing from the response or has invalid value + * @throws CertificateLevelMismatchException when the returned certificate level is lower than the requested one + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (requestedCertificateLevel == null) { + throw new SmartIdClientException("Parameter 'requestedCertificateLevel' is not provided"); + } + validateResult(sessionStatus.getResult()); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + validateSessionStatusCertificate(sessionCertificate); + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + X509Certificate certificate = getValidateX509Certificate(sessionCertificate, certificateLevel, requestedCertificateLevel); + return toCertificateChoiceResponse(sessionStatus, certificate, certificateLevel); + } + + private X509Certificate getValidateX509Certificate(SessionCertificate sessionCertificate, + CertificateLevel certificateLevel, + CertificateLevel requestedCertificateLevel) { + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException("Certificate choice session status response certificate level is lower than requested"); + } + X509Certificate certificate = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + certificateValidator.validate(certificate); + + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); + return certificate; + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result' is missing"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.endResult' is empty"); + } + if (!"OK".equalsIgnoreCase(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.documentNumber' is empty"); + } + } + + private static void validateSessionStatusCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert' is missing"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.value' has empty value"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has empty value"); + } + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Certificate choice session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has unsupported value"); + } + } + + private static CertificateChoiceResponse toCertificateChoiceResponse(SessionStatus sessionStatus, + X509Certificate certificate, + CertificateLevel certificateLevel) { + var certificateChoiceResponse = new CertificateChoiceResponse(); + certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); + certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); + certificateChoiceResponse.setCertificate(certificate); + certificateChoiceResponse.setCertificateLevel(certificateLevel); + certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return certificateChoiceResponse; + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index f4104ffc..ac4f5e43 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -347,19 +347,19 @@ private AuthenticationSessionRequest createAuthenticationRequest() { } private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionID())) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionID())) { throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionToken())) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionToken())) { throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.getSessionSecret())) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionSecret())) { throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); } - if (deviceLinkAuthenticationSessionResponse.getDeviceLinkBase() == null - || deviceLinkAuthenticationSessionResponse.getDeviceLinkBase().toString().isBlank()) { + if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null + || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index 74b15db0..13beadac 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -143,7 +143,6 @@ public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(S * Starts a device link-based certificate choice session and returns the session response. * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, * which can be used by the Relying Party to manage and verify the session independently. - *

    * * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. @@ -171,24 +170,15 @@ private void validateRequestParameters() { } private CertificateChoiceSessionRequest createCertificateRequest() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - request.setNonce(nonce); - request.setCapabilities(capabilities); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - request.setInitialCallbackUrl(initialCallbackUrl); - - return request; + return new CertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl + ); } private void validateInitialCallbackUrl() { @@ -198,19 +188,20 @@ private void validateInitialCallbackUrl() { } private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionID())) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionID())) { throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionToken())) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionToken())) { throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.getSessionSecret())) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionSecret())) { throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); } - if (deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase() == null || deviceLinkCertificateChoiceSessionResponse.getDeviceLinkBase().toString().isBlank()) { + if (deviceLinkCertificateChoiceSessionResponse.deviceLinkBase() == null + || deviceLinkCertificateChoiceSessionResponse.deviceLinkBase().toString().isBlank()) { throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); } } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index ec2fb95a..3de562a3 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -318,18 +318,18 @@ private void validateInitialCallbackUrl() { } private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionID())) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionToken())) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.getSessionSecret())) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); } - if (deviceLinkSignatureSessionResponse.getDeviceLinkBase() == null || deviceLinkSignatureSessionResponse.getDeviceLinkBase().toString().isBlank()) { + if (deviceLinkSignatureSessionResponse.deviceLinkBase() == null || deviceLinkSignatureSessionResponse.deviceLinkBase().toString().isBlank()) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); } } diff --git a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java new file mode 100644 index 00000000..84b674c4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java @@ -0,0 +1,76 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a nonqualified Smart-ID certificate and can be used for digital signing. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + */ +public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSignatureCertificatePurposeValidator.class); + + private static final Set NONQUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); + + @Override + public void validate(X509Certificate certificate) { + validateCertificateHasNonQualifiedSmartIdCertificatePolicies(certificate); + validateCertificateCanBeUsedForSigning(certificate); + } + + private static void validateCertificateHasNonQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); + } + if (!certificatePolicyOids.containsAll(NONQUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Non-qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", NONQUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate does not contain required non-qualified certificate policy OIDs"); + } + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 606be819..ab4d1174 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,19 +26,19 @@ * #L% */ +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.StringUtil; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; - -import java.util.Set; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.StringUtil; public class NotificationCertificateChoiceSessionRequestBuilder { @@ -179,23 +179,15 @@ private void validateRequestParameters() { } private CertificateChoiceSessionRequest createCertificateChoiceRequest() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID(relyingPartyUUID); - request.setRelyingPartyName(relyingPartyName); - - if (certificateLevel != null) { - request.setCertificateLevel(certificateLevel.name()); - } - - request.setNonce(nonce); - - if (this.shareMdClientIpAddress != null) { - var requestProperties = new RequestProperties(this.shareMdClientIpAddress); - request.setRequestProperties(requestProperties); - } - - request.setCapabilities(capabilities); - return request; + return new CertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null, + null + ); } private void validateNonce() { diff --git a/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java new file mode 100644 index 00000000..f0489e4b --- /dev/null +++ b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java @@ -0,0 +1,128 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a qualified Smart-ID certificate and can be used for digital signing. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Qualified profile + *

    + * Additionally, it will check that certificate can be used for qualified electronic signature by checking + * presence of QCStatements extension and that it contains the electronic signature OID. + */ +public class QualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + private static final Logger logger = LoggerFactory.getLogger(QualifiedSignatureCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); + + @Override + public void validate(X509Certificate certificate) { + validateCertificateHasQualifiedSmartIdCertificatePolicies(certificate); + validateCertificateCanBeUsedForSigning(certificate); + validateCertificateCanBeUsedForQualifiedElectronicSignature(certificate); + } + + private static void validateCertificateHasQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate does not contain required qualified certificate policy OIDs"); + } + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } + + private static void validateCertificateCanBeUsedForQualifiedElectronicSignature(X509Certificate certificate) { + byte[] extensionValue = certificate.getExtensionValue(Extension.qCStatements.getId()); + if (extensionValue == null) { + throw new UnprocessableSmartIdResponseException("Certificate does not have 'QCStatements' extension"); + } + if (!hasElectronicSigningOid(extensionValue)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have electronic signature OID (" + ETSIQCObjectIdentifiers.id_etsi_qct_esign.getId() + ") in QCStatements extension."); + } + } + + private static boolean hasElectronicSigningOid(byte[] extensionValue) { + ASN1Primitive prim; + try { + prim = ASN1Primitive.fromByteArray(ASN1OctetString.getInstance(extensionValue).getOctets()); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse QCStatements extension", ex); + } + + ASN1Sequence qcStatements = ASN1Sequence.getInstance(prim); + for (int i = 0; i < qcStatements.size(); i++) { + QCStatement qs = QCStatement.getInstance(qcStatements.getObjectAt(i)); + ASN1ObjectIdentifier stmtId = qs.getStatementId(); + + if (ETSIQCObjectIdentifiers.id_etsi_qcs_QcType.equals(stmtId)) { + ASN1Sequence typeSeq = ASN1Sequence.getInstance(qs.getStatementInfo()); + if (typeSeq == null) { + return false; + } + for (int j = 0; j < typeSeq.size(); j++) { + ASN1ObjectIdentifier typeOid = ASN1ObjectIdentifier.getInstance(typeSeq.getObjectAt(j)); + if (ETSIQCObjectIdentifiers.id_etsi_qct_esign.equals(typeOid)) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java new file mode 100644 index 00000000..34b7174c --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java @@ -0,0 +1,42 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public interface SignatureCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for digital signing + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for digital signing + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java new file mode 100644 index 00000000..41b039d1 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java @@ -0,0 +1,38 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +public interface SignatureCertificatePurposeValidatorFactory { + + /** + * Creates SignatureCertificatePurposeValidator based on the provided certificate level. + * + * @param certificateLevel the certificate level to create the validator for + * @return SignatureCertificatePurposeValidator instance + */ + SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java new file mode 100644 index 00000000..d89f9b80 --- /dev/null +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java @@ -0,0 +1,44 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { + + @Override + public SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel) { + if (certificateLevel == null) { + throw new SmartIdClientException("Parameter 'certificateLevel' is not provided"); + } + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedSignatureCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedSignatureCertificatePurposeValidator(); + default -> throw new SmartIdClientException("Unsupported certificate level: " + certificateLevel); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index 0b5b5e15..9e7c00cb 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -40,8 +40,8 @@ public class SignatureResponse implements Serializable { private SignatureAlgorithm signatureAlgorithm; private FlowType flowType; private X509Certificate certificate; - private String requestedCertificateLevel; - private String certificateLevel; + private CertificateLevel requestedCertificateLevel; + private CertificateLevel certificateLevel; private String documentNumber; private String interactionFlowUsed; private String deviceIpAddress; @@ -104,19 +104,19 @@ public void setCertificate(X509Certificate certificate) { this.certificate = certificate; } - public String getCertificateLevel() { + public CertificateLevel getCertificateLevel() { return certificateLevel; } - public void setCertificateLevel(String certificateLevel) { + public void setCertificateLevel(CertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; } - public String getRequestedCertificateLevel() { + public CertificateLevel getRequestedCertificateLevel() { return requestedCertificateLevel; } - public void setRequestedCertificateLevel(String requestedCertificateLevel) { + public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { this.requestedCertificateLevel = requestedCertificateLevel; } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index 25bd79ad..572738dc 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -26,23 +26,14 @@ * #L% */ -import java.io.IOException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.regex.Pattern; -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.asn1.x509.qualified.QCStatement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,25 +56,29 @@ public class SignatureResponseValidator { private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); - private static final Set QUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); - private static final Set NONQUALIFIED_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); - private static final int KEYUSAGE_NON_REPUDIATION_INDEX = 1; - private static final String QC_STATEMENT_OID = "1.3.6.1.5.5.7.1.3"; - private static final String ELECTRONIC_SIGNING = "0.4.0.1862.1.6.1"; private final CertificateValidator certificateValidator; - private final boolean qcStatementRequired; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; - public SignatureResponseValidator(CertificateValidator certificateValidator, boolean qcRequired) { + /** + * Initializes the validator with a {@link CertificateValidator} and a {@link SignatureCertificatePurposeValidatorFactory}. + * + * @param certificateValidator the certificate validator + * @param signatureCertificatePurposeValidatorFactory the signature certificate purpose validator factory + */ + public SignatureResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { this.certificateValidator = certificateValidator; - this.qcStatementRequired = qcRequired; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; } /** - * Initializes the validator with a {@link CertificateValidator}. + * Initializes the validator with a {@link CertificateValidator} + * + * @param certificateValidator the certificate validator */ public SignatureResponseValidator(CertificateValidator certificateValidator) { - this(certificateValidator, false); + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); } /** @@ -100,7 +95,7 @@ public SignatureResponseValidator(CertificateValidator certificateValidator) { * @throws SmartIdClientException if any of method parameters are not provided */ public SignatureResponse validate(SessionStatus sessionStatus, - String requestedCertificateLevel + CertificateLevel requestedCertificateLevel ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { validateSessionsStatus(sessionStatus, requestedCertificateLevel); @@ -125,7 +120,7 @@ public SignatureResponse validate(SessionStatus sessionStatus, signatureResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); - signatureResponse.setCertificateLevel(certificate.getCertificateLevel()); + signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); @@ -133,7 +128,7 @@ public SignatureResponse validate(SessionStatus sessionStatus, return signatureResponse; } - private void validateSessionsStatus(SessionStatus sessionStatus, String requestedCertificateLevel) { + private void validateSessionsStatus(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { if (sessionStatus == null) { throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); } @@ -149,7 +144,7 @@ private void validateSessionsStatus(SessionStatus sessionStatus, String requeste validateSessionResult(sessionStatus, requestedCertificateLevel); } - private void validateSessionResult(SessionStatus sessionStatus, String requestedCertificateLevel) { + private void validateSessionResult(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { SessionResult sessionResult = sessionStatus.getResult(); if (sessionResult == null) { @@ -178,43 +173,31 @@ private void validateSessionResult(SessionStatus sessionStatus, String requested } } - private void validateCertificate(SessionCertificate sessionCertificate, String requestedCertificateLevel) { + private void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing or empty"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing"); } if (StringUtil.isEmpty(sessionCertificate.getValue())) { throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); } - - X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); - if (!isCertificateLevelValid(requestedCertificateLevel, sessionCertificate.getCertificateLevel())) { - logger.error("Signature session status certificate level mismatch: requested {}, returned {}", requestedCertificateLevel, sessionCertificate.getCertificateLevel()); + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Signature session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' has unsupported value"); + } + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + logger.error("Signature session status certificate level mismatch: requested {}, returned {}", + requestedCertificateLevel, sessionCertificate.getCertificateLevel()); throw new CertificateLevelMismatchException(); } + X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); certificateValidator.validate(certificate); - validateCertificatePoliciesAndPurpose(certificate); - } - - private void validateCertificatePoliciesAndPurpose(X509Certificate cert) { - Set oids = getPolicyOids(cert); - boolean hasAllQualified = oids.containsAll(QUALIFIED_POLICY_OIDS); - boolean hasAllNonQual = oids.containsAll(NONQUALIFIED_POLICY_OIDS); - if (!hasAllQualified && !hasAllNonQual) { - throw new UnprocessableSmartIdResponseException("CertificatePolicies missing required Smart-ID OIDs"); - } - - boolean[] keyUsage = cert.getKeyUsage(); - if (keyUsage == null || keyUsage.length < 2 || !keyUsage[KEYUSAGE_NON_REPUDIATION_INDEX]) { - throw new UnprocessableSmartIdResponseException("KeyUsage must contain NonRepudiation"); - } - if (qcStatementRequired && !containsQcStatement(cert)) { - throw new UnprocessableSmartIdResponseException("QCStatement 0.4.0.1862.1.6.1 missing"); - } + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); } private static X509Certificate parseAndCheckCertificate(String certBase64) { @@ -228,55 +211,6 @@ private static X509Certificate parseAndCheckCertificate(String certBase64) { return certificate; } - private static boolean isCertificateLevelValid(String requestedCertificateLevel, String returnedCertificateLevel) { - CertificateLevel requestedLevel = CertificateLevel.valueOf(requestedCertificateLevel.toUpperCase()); - CertificateLevel returnedLevel = CertificateLevel.valueOf(returnedCertificateLevel.toUpperCase()); - - return returnedLevel.isSameLevelOrHigher(requestedLevel); - } - - private static Set getPolicyOids(X509Certificate certificate) { - Set result = new HashSet<>(); - byte[] extensionValue = certificate.getExtensionValue("2.5.29.32"); - if (extensionValue == null) { - return result; - } - try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { - ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); - try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { - CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); - for (PolicyInformation pi : policies.getPolicyInformation()) { - result.add(pi.getPolicyIdentifier().getId()); - } - } - } catch (IOException ex) { - throw new UnprocessableSmartIdResponseException("Unable to parse certificate policies", ex); - } - return result; - } - - private static boolean containsQcStatement(X509Certificate cert) { - byte[] extensionValue = cert.getExtensionValue(QC_STATEMENT_OID); - if (extensionValue == null) { - return false; - } - try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { - ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); - try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { - ASN1Sequence seq = (ASN1Sequence) ais2.readObject(); - for (int i = 0; i < seq.size(); i++) { - QCStatement st = QCStatement.getInstance(seq.getObjectAt(i)); - if (ELECTRONIC_SIGNING.equals(st.getStatementId().getId())) { - return true; - } - } - } - } catch (IOException ex) { - throw new UnprocessableSmartIdResponseException("Unable to parse QCStatements", ex); - } - return false; - } - private static void validateSignature(SessionStatus sessionStatus) { String signatureProtocol = sessionStatus.getSignatureProtocol(); diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java index c016c7b1..ee73edf1 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -44,18 +44,6 @@ public final class SignatureValueValidatorImpl implements SignatureValueValidato private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); - private static SignatureValueValidatorImpl INSTANCE; - - private SignatureValueValidatorImpl() { - } - - public static SignatureValueValidatorImpl getInstance() { - if (INSTANCE == null) { - INSTANCE = new SignatureValueValidatorImpl(); - } - return INSTANCE; - } - @Override public void validate(byte[] signatureValue, byte[] payload, diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java index de256abb..e203a1fa 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java @@ -31,79 +31,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; -public class CertificateChoiceSessionRequest implements Serializable { +public record CertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { - private String relyingPartyUUID; - private String relyingPartyName; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String certificateLevel; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String nonce; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private Set capabilities; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private RequestProperties requestProperties; - - @JsonInclude(JsonInclude.Include.NON_NULL) - private String initialCallbackUrl; - - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - public String getCertificateLevel() { - return certificateLevel; - } - - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } - - public String getNonce() { - return nonce; - } - - public void setNonce(String nonce) { - this.nonce = nonce; - } - - public Set getCapabilities() { - return capabilities; - } - - public void setCapabilities(Set capabilities) { - this.capabilities = capabilities; - } - - public RequestProperties getRequestProperties() { - return requestProperties; - } - - public void setRequestProperties(RequestProperties requestProperties) { - this.requestProperties = requestProperties; - } - - public String getInitialCallbackUrl() { - return initialCallbackUrl; - } - - public void setInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index 5795270c..2ca258f3 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -30,57 +30,24 @@ import java.net.URI; import java.time.Instant; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) -public class DeviceLinkSessionResponse implements Serializable { - - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private final Instant receivedAt; - - private String sessionID; - private String sessionToken; - private String sessionSecret; - private URI deviceLinkBase; - - public DeviceLinkSessionResponse() { - receivedAt = Instant.now(); - } - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } - - public String getSessionToken() { - return sessionToken; - } - - public void setSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - } - - public String getSessionSecret() { - return sessionSecret; - } - - public void setSessionSecret(String sessionSecret) { - this.sessionSecret = sessionSecret; - } - - public Instant getReceivedAt() { - return receivedAt; - } - - public URI getDeviceLinkBase() { - return deviceLinkBase; - } - - public void setDeviceLinkBase(URI deviceLinkBase) { - this.deviceLinkBase = deviceLinkBase; +public record DeviceLinkSessionResponse(String sessionID, + String sessionToken, + String sessionSecret, + URI deviceLinkBase, + Instant receivedAt + +) implements Serializable { + + @JsonCreator + public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, + @JsonProperty("sessionToken") String sessionToken, + @JsonProperty("sessionSecret") String sessionSecret, + @JsonProperty("deviceLinkBase") URI deviceLinkBase) { + this(sessionID, sessionToken, sessionSecret, deviceLinkBase, Instant.now()); } } diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index 7efbd174..bc903f55 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,12 +34,15 @@ import java.time.ZoneOffset; import java.util.Date; import java.util.Enumeration; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1GeneralizedTime; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DLSequence; @@ -48,15 +51,25 @@ import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.CertificatePolicies; import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.PolicyInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public final class CertificateAttributeUtil { -public class CertificateAttributeUtil { private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); + private static final String CERTIFICATE_POLICY_OID = "2.5.29.32"; + private static final int KEY_USAGE_NON_REPUDIATION_INDEX = 1; + + private CertificateAttributeUtil() { + } + /** * Get Date-of-birth (DoB) from a specific certificate header (if present). *

    @@ -79,7 +92,6 @@ public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { * @param oid Object Identifier (OID) of the attribute to extract * @return Attribute value */ - public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { var x500name = new X500Name(distinguishedName); RDN[] rdns = x500name.getRDNs(oid); @@ -89,6 +101,47 @@ public static Optional getAttributeValue(String distinguishedName, ASN1O return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); } + /** + * Extracts certificate policy OID from the given X.509 certificate. + * + * @param certificate the X.509 certificate from which to extract the policy OIDs + * @return a set of certificate policy OIDs as strings; an empty set if no policies are found + * @throws SmartIdClientException if there is an error parsing the certificate policies + */ + public static Set getCertificatePolicy(X509Certificate certificate) { + Set result = new HashSet<>(); + byte[] extensionValue = certificate.getExtensionValue(CERTIFICATE_POLICY_OID); + if (extensionValue == null) { + return result; + } + try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { + ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); + try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { + CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); + for (PolicyInformation pi : policies.getPolicyInformation()) { + result.add(pi.getPolicyIdentifier().getId()); + } + } + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse certificate policies", ex); + } + return result; + } + + /** + * Checks if the certificate has KeyUsage extension with Non-Repudiation bit set + *

    + * This method can be used to check if a certificate is valid for signing in case the certificate profile + * requires that Non-Repudiation bit must be set in KeyUsage extension. + * + * @param certificate the X.509 certificate to check + * @return true if the certificate does not have KeyUsage extension or does not have Non-Repudiation bit set; false otherwise + */ + public static boolean hasNonRepudiationKeyUsage(X509Certificate certificate) { + boolean[] keyUsage = certificate.getKeyUsage(); + return keyUsage != null && keyUsage.length > 1 && keyUsage[KEY_USAGE_NON_REPUDIATION_INDEX]; + } + private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { try { return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); @@ -144,5 +197,4 @@ private static ASN1Primitive toDerObject(byte[] data) throws IOException { return asnInputStream.readObject(); } - } diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java index 65960742..2da25525 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,9 +28,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.ByteArrayInputStream; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.LocalDate; import java.util.Optional; @@ -39,11 +36,11 @@ class AuthenticationIdentityMapperTest { - private static final byte[] AUTH_CERT = FileUtil.readFileBytes("test-certs/auth-cert-40504040001.pem.crt"); + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); @Test void from() { - X509Certificate certificate = getX509Certificate(); + X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -54,13 +51,4 @@ void from() { assertEquals(certificate, authenticationIdentity.getAuthCertificate()); assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); } - - private static X509Certificate getX509Certificate() { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(AUTH_CERT)); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index b3cb9bfc..a671136a 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -30,12 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -64,21 +58,21 @@ class AuthenticationResponseMapperImplTest { @BeforeEach void setUp() { - authenticationResponseMapper = AuthenticationResponseMapperImpl.getInstance(); + authenticationResponseMapper = new AuthenticationResponseMapperImpl(); } @Test void from() { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); assertEquals("OK", authenticationResponse.getEndResult()); assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); @@ -91,14 +85,14 @@ void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); var sessionSignature = toSessionSignature("rsassa-pss"); sessionSignature.setFlowType(flowType.getDescription()); - var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); assertEquals("OK", authenticationResponse.getEndResult()); assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); @@ -113,14 +107,14 @@ void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorit sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); - var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); assertEquals("OK", authenticationResponse.getEndResult()); assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); @@ -199,7 +193,6 @@ void from_documentNumberIsEmpty_throwException(String documentNumber) { } } - @ParameterizedTest @NullAndEmptySource void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { @@ -429,7 +422,6 @@ void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); @@ -761,7 +753,7 @@ void from_certificateIsInvalid_throwException() { void from_certificateLevelIsInvalid_throwException() { var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(getEncodedCertificateData(AUTH_CERT), "invalid"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "invalid"); var sessionStatus = new SessionStatus(); sessionStatus.setResult(sessionResult); @@ -842,19 +834,4 @@ private static SessionStatus toSessionStatus(SessionResult sessionResult, Sessio sessionStatus.setDeviceIpAddress("0.0.0.0"); return sessionStatus; } - - private static X509Certificate toX509Certificate(String certificateValue) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateValue.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - private static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); - } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 9d312150..3db5f281 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -197,7 +197,7 @@ private static SessionStatus toSessionsStatus(String certificateValue, String ce signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); var cert = new SessionCertificate(); - cert.setValue(getEncodedCertificateData(certificateValue)); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); cert.setCertificateLevel(certificateLevel); var sessionStatus = new SessionStatus(); @@ -223,14 +223,7 @@ private static AuthenticationSessionRequest toAuthenticationSessionRequest(Strin null); } - private static String toBase64(String data) { return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); } - - private static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); - } } diff --git a/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java deleted file mode 100644 index 3d355a7f..00000000 --- a/src/test/java/ee/sk/smartid/CertificateChoiceResponseMapperTest.java +++ /dev/null @@ -1,284 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionStatus; - -public class CertificateChoiceResponseMapperTest { - - private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - - @Test - void from() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); - sessionCertificate.setCertificateLevel("QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(toX509Certificate(), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void from_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); - sessionCertificate.setCertificateLevel("QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus, requestedCertificateLevel); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(toX509Certificate(), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void from_returnedCertificateHigherThanRequested_ok() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); - sessionCertificate.setCertificateLevel("QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus, CertificateLevel.ADVANCED); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(toX509Certificate(), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void from_expiredCertificateWasReturned() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(getEncodedCertificateData(EXPIRED_CERT)); - sessionCertificate.setCertificateLevel("QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Signer's certificate is not valid", ex.getMessage()); - } - - @Test - void from_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(getEncodedCertificateData(CERTIFICATE_CHOICE_CERT)); - sessionCertificate.setCertificateLevel("ADVANCED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Certificate level returned by Smart-ID is lower than requested", ex.getMessage()); - } - - @Test - void from_sessionCertificateLevelIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("INVALID"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Certificate level parameter is missing in certificate", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Value parameter is missing in certificate", ex.getMessage()); - } - - @Test - void from_sessionCertificateIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Certificate parameter is missing in session status", ex.getMessage()); - } - - @Test - void from_sessionDocumentNumberIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("Document number parameter is missing in the session result", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void from_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - } - - @Test - void from_sessionEndResultIsNotProvided_throwException() { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(new SessionResult()); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(sessionStatus)); - assertEquals("End result parameter is missing in the session result", ex.getMessage()); - } - - @Test - void from_sessionResultIsNotProvided_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> CertificateChoiceResponseMapper.from(new SessionStatus())); - assertEquals("Session result parameter is missing", ex.getMessage()); - } - - @Test - void from_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> CertificateChoiceResponseMapper.from(null)); - assertEquals("Session status parameter is not provided", ex.getMessage()); - } - - private static X509Certificate toX509Certificate() { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(CERTIFICATE_CHOICE_CERT.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - private static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); - } -} diff --git a/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java new file mode 100644 index 00000000..9a1b1d21 --- /dev/null +++ b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java @@ -0,0 +1,290 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionStatus; + +public class CertificateChoiceResponseValidatorTest { + + private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + + CertificateChoiceResponseValidator certificateChoiceResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + } + + @Test + void validate() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_returnedCertificateHigherThanRequested_ok() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_nqCertificate() { + var sessionStatus = toSessionStatus(NQ_SIGNING_CERTIFICATE, "ADVANCED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE), response.getCertificate()); + assertEquals(CertificateLevel.ADVANCED, response.getCertificateLevel()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_requestCertificateLevelNotProvided_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus, null)); + assertEquals("Parameter 'requestedCertificateLevel' is not provided", ex.getMessage()); + } + } + + @Nested + class ValidateEndResult { + + @Test + void validate_sessionResultIsNotProvided_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(new SessionStatus())); + assertEquals("Certificate choice session status field 'result' is missing", ex.getMessage()); + } + + @Test + void validate_sessionEndResultIsNotProvided_throwException() { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(new SessionResult()); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.endResult' is empty", ex.getMessage()); + } + + @Test + void validate_sessionDocumentNumberIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.documentNumber' is empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void validate_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + } + + @Nested + class ValidateCertificate { + + @Test + void validate_sessionCertificateIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.value' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotSupported_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + sessionCertificate.setCertificateLevel("invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } + + @Test + void validate_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status response certificate level is lower than requested", ex.getMessage()); + } + + @Test + void validate_expiredCertificateWasReturned() { + var sessionStatus = toSessionStatus(EXPIRED_CERT, "QUALIFIED"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate is invalid", ex.getMessage()); + } + } + + private static SessionStatus toSessionStatus(String certificateChoiceCert, String certificateLevel) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(certificateChoiceCert)); + sessionCertificate.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java index 5b084b9b..6e5fad02 100644 --- a/src/test/java/ee/sk/smartid/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -27,6 +27,7 @@ */ import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -36,19 +37,34 @@ public final class CertificateUtil { private CertificateUtil() { } - public static byte[] getX509CertificateBytes(String base64Certificate) { - String caCertificateInPem = CertificateParser.BEGIN_CERT + "\n" + base64Certificate + "\n" + CertificateParser.END_CERT; - return caCertificateInPem.getBytes(); - } - - public static X509Certificate getX509Certificate(byte[] certificateBytes) throws CertificateException { + public static X509Certificate toX509Certificate(byte[] certificateBytes) throws CertificateException { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); } - public static X509Certificate getX509Certificate(String base64Certificate) throws CertificateException { + public static X509Certificate toX509Certificate(String certificate) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + public static X509Certificate toX509CertificateFromEncodedString(String base64Certificate) throws CertificateException { byte[] certificateBytes = getX509CertificateBytes(base64Certificate); - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + return toX509Certificate(certificateBytes); } + + public static String getEncodedCertificateData(String certificate) { + return certificate.replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\n", ""); + } + + private static byte[] getX509CertificateBytes(String encodedData) { + String certificate = CertificateParser.BEGIN_CERT + "\n" + encodedData + "\n" + CertificateParser.END_CERT; + return certificate.getBytes(StandardCharsets.UTF_8); + } + } diff --git a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java index 9808a1f8..a3e29fa7 100644 --- a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -54,28 +54,24 @@ void setUp() { @Test void validate_ok() throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + X509Certificate certificate = CertificateUtil.toX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); assertDoesNotThrow(() -> certificateValidator.validate(certificate)); } @Test void validate_expired() throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); + X509Certificate certificate = CertificateUtil.toX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - certificateValidator.validate(certificate); - }); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); assertEquals("Certificate is invalid", exception.getMessage()); } @Test void validate_notTrusted() throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + X509Certificate certificate = CertificateUtil.toX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - certificateValidator.validate(certificate); - }); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); assertEquals("Certificate chain validation failed", exception.getMessage()); } } diff --git a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java index 7d767574..485f5d81 100644 --- a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,10 +26,6 @@ * #L% */ -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.List; @@ -46,9 +42,9 @@ class DefaultTrustedCAStoreBuilderTest { @Test void buildDefaultTrustedCACertStore_ocspValidationDisabled() { - X509Certificate trustAnchorCertificate = toX509Certificate(TRUST_ANCHOR_CERT); + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = toX509Certificate(INTERMEDIATE_CA_CERT); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); new DefaultTrustedCAStoreBuilder() .withTrustAnchors(Set.of(trustAnchor)) .withIntermediateCACertificate(List.of(intermediateCACertificate)) @@ -59,23 +55,14 @@ void buildDefaultTrustedCACertStore_ocspValidationDisabled() { @Disabled("Fails with OCSP response validation error, needs investigation") @Test void buildDefaultTrustedCACertStore_ocspValidationEnabled() { - X509Certificate trustAnchorCertificate = toX509Certificate(TRUST_ANCHOR_CERT); + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = toX509Certificate(INTERMEDIATE_CA_CERT); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); new DefaultTrustedCAStoreBuilder() .withTrustAnchors(Set.of(trustAnchor)) .withIntermediateCACertificate(List.of(intermediateCACertificate)) .withOcspEnabled(true) - .withOCSPValidationCert(toX509Certificate(OCSP_CERT)) + .withOCSPValidationCert(CertificateUtil.toX509Certificate(OCSP_CERT)) .build(); } - - private X509Certificate toX509Certificate(String certificate) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 9d967382..6ee36e54 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -428,8 +428,7 @@ class ValidateRequiredResponseParameters { @NullAndEmptySource void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID(sessionId); + var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); initAuthentication(); @@ -441,9 +440,7 @@ void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException @NullAndEmptySource void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); - deviceLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - deviceLinkAuthenticationSessionResponse.setSessionToken(sessionToken); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); initAuthentication(); @@ -455,10 +452,7 @@ void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwExcept @NullAndEmptySource void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); - dynamicLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - dynamicLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); - dynamicLinkAuthenticationSessionResponse.setSessionSecret(sessionSecret); + var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); initAuthentication(); @@ -470,12 +464,7 @@ void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwExcep @NullAndEmptySource void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("00000000-0000-0000-0000-000000000000"); - response.setSessionToken(generateBase64String("sessionToken")); - response.setSessionSecret(generateBase64String("sessionSecret")); - response.setDeviceLinkBase(deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - + var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000",generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue) ); when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(response); initAuthentication(); }); @@ -537,12 +526,10 @@ void initAuthenticationSession_withDocumentNumber() { } private DeviceLinkSessionResponse createDynamicLinkAuthenticationResponse() { - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(); - deviceLinkAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - deviceLinkAuthenticationSessionResponse.setSessionToken(generateBase64String("sessionToken")); - deviceLinkAuthenticationSessionResponse.setSessionSecret(generateBase64String("sessionSecret")); - deviceLinkAuthenticationSessionResponse.setDeviceLinkBase(URI.create("https://example.com/callback")); - return deviceLinkAuthenticationSessionResponse; + return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", + generateBase64String("sessionToken"), + generateBase64String("sessionSecret"), + URI.create("https://example.com/callback")); } private static String generateBase64String(String text) { diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index 3b4656c0..616ca6c9 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -79,10 +79,10 @@ void initiateCertificateChoice() { DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @@ -95,10 +95,10 @@ void initiateCertificateChoice_nullRequestProperties() { DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @@ -122,10 +122,10 @@ void initiateCertificateChoice_withValidCapabilities() { DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @@ -138,10 +138,10 @@ void initiateCertificateChoice_nullCapabilities() { DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - assertEquals("test-session-id", result.getSessionID()); - assertEquals("test-session-token", result.getSessionToken()); - assertEquals("test-session-secret", result.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.getDeviceLinkBase()); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); } @@ -152,12 +152,12 @@ class ErrorCases { @ParameterizedTest @NullAndEmptySource void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { - var responseWithNullSessionID = new DeviceLinkSessionResponse(); - responseWithNullSessionID.setSessionID(sessionId); - responseWithNullSessionID.setSessionToken("test-session-token"); - responseWithNullSessionID.setSessionSecret("test-session-secret"); - responseWithNullSessionID.setDeviceLinkBase(URI.create("https://example.com/device-link")); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(responseWithNullSessionID); + var response = new DeviceLinkSessionResponse(sessionId, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link"), + null); + when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); @@ -166,11 +166,10 @@ void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { @ParameterizedTest @NullAndEmptySource void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken(sessionToken); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); @@ -181,11 +180,10 @@ void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken @ParameterizedTest @NullAndEmptySource void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret(sessionSecret); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); @@ -196,11 +194,10 @@ void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecr @ParameterizedTest @NullAndEmptySource void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(uriString == null ? null : URI.create(uriString)); + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + uriString == null ? null : URI.create(uriString)); when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); @@ -233,7 +230,7 @@ void initiateCertificateChoice_missingRelyingPartyName() { } @ParameterizedTest - @ValueSource(strings = {"","1234567890123456789012345678901" }) + @ValueSource(strings = {"", "1234567890123456789012345678901"}) void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { builderService.withNonce(invalidNonce); @@ -278,12 +275,10 @@ void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url } private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); - return response; + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); } private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index c5cf9d9c..0ba350b2 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -93,10 +93,10 @@ void initSignatureSession_withSemanticsIdentifier() { DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - assertEquals("test-session-id", signature.getSessionID()); - assertEquals("test-session-token", signature.getSessionToken()); - assertEquals("test-session-secret", signature.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); + assertEquals("test-session-id", signature.sessionID()); + assertEquals("test-session-token", signature.sessionToken()); + assertEquals("test-session-secret", signature.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); @@ -114,10 +114,10 @@ void initSignatureSession_withDocumentNumber() { DeviceLinkSessionResponse signature = builder.initSignatureSession(); assertNotNull(signature); - assertEquals("test-session-id", signature.getSessionID()); - assertEquals("test-session-token", signature.getSessionToken()); - assertEquals("test-session-secret", signature.getSessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signature.getDeviceLinkBase()); + assertEquals("test-session-id", signature.sessionID()); + assertEquals("test-session-token", signature.sessionToken()); + assertEquals("test-session-secret", signature.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(documentNumber)); @@ -386,11 +386,10 @@ class ResponseValidationTests { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionID(String sessionID) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID(sessionID); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + var response = new DeviceLinkSessionResponse(sessionID, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); @@ -402,11 +401,10 @@ void validateResponseParameters_missingSessionID(String sessionID) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionToken(String sessionToken) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken(sessionToken); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); @@ -418,11 +416,10 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { @ParameterizedTest @NullAndEmptySource void validateResponseParameters_missingSessionSecret(String sessionSecret) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret(sessionSecret); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); @@ -434,11 +431,10 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { @ParameterizedTest @NullAndEmptySource void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); @@ -449,12 +445,10 @@ void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String d } private DeviceLinkSessionResponse mockSignatureSessionResponse() { - var response = new DeviceLinkSessionResponse(); - response.setSessionID("test-session-id"); - response.setSessionToken("test-session-token"); - response.setSessionSecret("test-session-secret"); - response.setDeviceLinkBase(URI.create("https://example.com/device-link")); - return response; + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); } private static class CertificateLevelArgumentProvider implements ArgumentsProvider { diff --git a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java new file mode 100644 index 00000000..668d099e --- /dev/null +++ b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java @@ -0,0 +1,112 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +public final class InvalidCertificateGenerator { + + private InvalidCertificateGenerator() { + } + + public static X509Certificate createCertificate(CertificatePolicies policies, + KeyUsage keyUsage, + QCStatement qcStatement) { + Security.addProvider(new BouncyCastleProvider()); + KeyPair kp = createKeyPair(); + X509V3CertificateGenerator certGen = getBaseX509Generator(kp); + if (policies != null) { + certGen.addExtension(Extension.certificatePolicies, false, policies); + } + if (keyUsage != null) { + certGen.addExtension(Extension.keyUsage, true, keyUsage); + } + if (qcStatement != null) { + certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); + } + return generate(certGen, kp); + } + + public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { + ASN1EncodableVector vec = new ASN1EncodableVector(); + vec.addAll(policyInformations); + return CertificatePolicies.getInstance(new DERSequence(vec)); + } + + private static KeyPair createKeyPair() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { + X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); + X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); + + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setIssuerDN(issuer); + certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); + certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); + certGen.setSubjectDN(subject); + certGen.setPublicKey(kp.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return certGen; + } + + private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { + try { + return certGen.generateX509Certificate(kp.getPrivate(), "BC"); + } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java new file mode 100644 index 00000000..e7cd8392 --- /dev/null +++ b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java @@ -0,0 +1,120 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class NonQualifiedSignatureCertificatePurposeValidatorTest { + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; + + private NonQualifiedSignatureCertificatePurposeValidator validator; + + @BeforeEach + void setUp() { + Security.addProvider(new BouncyCastleProvider()); + validator = new NonQualifiedSignatureCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not contain required non-qualified certificate policy OIDs", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skNQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, keyUsage, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 67a2a9c3..f06714ab 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -109,7 +109,7 @@ void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certifica verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); CertificateChoiceSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.getCertificateLevel()); + assertEquals(expectedValue, request.certificateLevel()); } @ParameterizedTest @@ -130,7 +130,7 @@ void initCertificateChoiceSession_nonce_ok(String nonce) { verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); CertificateChoiceSessionRequest request = requestCaptor.getValue(); - assertEquals(nonce, request.getNonce()); + assertEquals(nonce, request.nonce()); } @Test @@ -150,7 +150,7 @@ void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestPropertie verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); CertificateChoiceSessionRequest request = requestCaptor.getValue(); - assertNull(request.getRequestProperties()); + assertNull(request.requestProperties()); } @ParameterizedTest @@ -172,8 +172,8 @@ void initCertificateChoiceSession_ipQueryingRequired_ok(boolean ipRequested) { verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); CertificateChoiceSessionRequest request = requestCaptor.getValue(); - assertNotNull(request.getRequestProperties()); - assertEquals(ipRequested, request.getRequestProperties().shareMdClientIpAddress()); + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); } @ParameterizedTest @@ -194,7 +194,7 @@ void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set validator.validate(CertificateUtil.toX509Certificate(QUALIFIED_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.createCertificate(null, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, keyUsage, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + @Test + void validate_QsStatementsExtensionIsMissing_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation), null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); + } + + @Test + void validate_QsStatementsDoesNotHaveElectronicSigning_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + + QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); + X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation), qcStatement); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index c3f03268..016f6d6b 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -55,6 +55,10 @@ class SignatureResponseValidatorTest { private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + // // TODO - 31.08.25: replace these values when the test accounts are available + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; + private SignatureResponseValidator signatureResponseValidator; @BeforeEach @@ -66,21 +70,50 @@ void setUp() { signatureResponseValidator = new SignatureResponseValidator(certificateValidator); } + @Test + void validate_validRawDigestSignature() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel); + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_nqSigning_ok() { + SessionStatus sessionStatus = toNqignatureSessionStatus(); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + assertEquals("OK", response.getEndResult()); + } + @Test void validate_stateParameterMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setState(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'state' is empty", ex.getMessage()); } @Test void validate_sessionNotComplete() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setState("RUNNING"); - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertTrue(ex.getMessage().contains("Session is not complete")); } @@ -90,34 +123,34 @@ void validate_sessionResultNull() { sessionStatus.setState("COMPLETE"); sessionStatus.setResult(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'result' is missing", ex.getMessage()); } @Test void validate_missingDocumentNumber() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getResult().setDocumentNumber(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); } @Test void validate_missingInteractionFlowUsed() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setInteractionTypeUsed(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); } @Test void validate_signatureProtocolMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); } @@ -126,91 +159,60 @@ class CertificateValidation { @Test void validate_missingCertificate() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setCert(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); - assertEquals("Signature session status field 'cert' is missing or empty", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert' is missing", ex.getMessage()); } @Test void validate_missingCertificateValue() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setValue(null); - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); } @Test void validate_certificateLevelMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setCertificateLevel(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); } @Test void validate_certificateLevelMismatch() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getCert().setCertificateLevel("ADVANCED"); - var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); } - - @Test - void validate_qcStatementRequiredButMissing() { - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - var validatorWithQcRequired = new SignatureResponseValidator(certificateValidator, true); - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validatorWithQcRequired.validate(sessionStatus, "QUALIFIED")); - assertEquals("QCStatement 0.4.0.1862.1.6.1 missing", ex.getMessage()); - } } @Nested class SignatureValidation { - @Test - void validate_validRawDigestSignature() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, "QUALIFIED"); - assertEquals("OK", response.getEndResult()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel.name()); - assertEquals("OK", response.getEndResult()); - assertEquals("QUALIFIED", response.getCertificateLevel()); - } - @Test void validate_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test void validate_unknownSignatureProtocol() { - SessionStatus sessionStatus = createMockSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); } @@ -224,7 +226,7 @@ void validate_handleSessionEndResultErrors(String endResult, Class signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); } @ParameterizedTest @@ -241,107 +243,107 @@ void validate_endResultIsUserRefusedInteraction(String interaction, Class signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); } @Test void validate_endResultMissing_throwsException() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); sessionStatus.getResult().setEndResult(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); } @Test void validate_sessionStatusNull() { - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, "QUALIFIED")); + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, CertificateLevel.QUALIFIED)); assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); } @Test void validate_signatureMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.setSignature(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); } @Test void validate_signatureValueMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setValue(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); } @Test void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setValue("invalid-not+encoded+value"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); } @Test void validate_signatureAlgorithmMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setSignatureAlgorithm(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); } @ParameterizedTest @ValueSource(strings = {"SHA-1", "invalid"}) void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test void validate_flowTypeMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setFlowType(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); } @Test void validate_invalidFlowType() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); } @Test void validate_signatureAlgorithmNotSupported() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @Test void validate_signatureAlgorithmNotRsassaPss() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); sessionStatus.getSignature().setSignatureAlgorithm("rsa"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); } @@ -351,40 +353,40 @@ class SignatureAlgorithmParametersValidations { @Test void validate_signatureAlgorithmParametersMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().setSignatureAlgorithmParameters(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_hashAlgorithmMissing(String hashAlgorithm) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); } @Test void validate_invalidHashAlgorithm() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); } @Test void validate_maskGenAlgorithmIsMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); } @@ -392,131 +394,114 @@ void validate_maskGenAlgorithmIsMissing() { @ParameterizedTest @NullAndEmptySource void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); } @Test void validate_invalidMaskGenAlgorithmName() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); } @Test void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() .setParameters(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() .getParameters().setHashAlgorithm(hashAlgorithm); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); } @Test void validate_maskGenHashAlgorithmInvalid() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() .getParameters().setHashAlgorithm("INVALID-HASH"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); } @Test void validate_mismatchedHashAlgorithms() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); } @Test void validate_saltLengthIsMissing() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); } @Test void validate_invalidSaltLength() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); } @Test void validate_invalidTrailerField() { - SessionStatus sessionStatus = createMockSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, "QUALIFIED")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); } } } - private static SessionStatus createMockSessionStatus(String signatureProtocol, String signatureAlgorithm) { - + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, String signatureAlgorithm) { var sessionResult = new SessionResult(); sessionResult.setEndResult("OK"); sessionResult.setDocumentNumber("PNOEE-12345678901"); var sessionCertificate = new SessionCertificate(); sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(getEncodedCertificateData()); - - var params = new SessionSignatureAlgorithmParameters(); - params.setHashAlgorithm("SHA-512"); - var mgf = new SessionMaskGenAlgorithm(); - mgf.setAlgorithm("id-mgf1"); - var mgfParams = new SessionMaskGenAlgorithmParameters(); - mgfParams.setHashAlgorithm("SHA-512"); - mgf.setParameters(mgfParams); - params.setMaskGenAlgorithm(mgf); - params.setSaltLength(64); - params.setTrailerField("0xbc"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("expectedDigest"); - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(params); - sessionSignature.setServerRandom("serverRandomValue"); - sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); - sessionSignature.setFlowType("QR"); + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params); var sessionStatus = new SessionStatus(); sessionStatus.setState("COMPLETE"); @@ -529,9 +514,55 @@ private static SessionStatus createMockSessionStatus(String signatureProtocol, S return sessionStatus; } - private static String getEncodedCertificateData() { - return SignatureResponseValidatorTest.SIGN_CERT.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\n", ""); + private static SessionStatus toNqignatureSessionStatus() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("ADVANCED"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); + + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(SignatureProtocol.RAW_DIGEST_SIGNATURE.name()); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static SessionSignature toSessionSignature(String signatureValue, + String signatureAlgorithm, + SessionSignatureAlgorithmParameters params) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(params); + sessionSignature.setServerRandom("serverRandomValue"); + sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); + sessionSignature.setFlowType("QR"); + return sessionSignature; + } + + private static SessionSignatureAlgorithmParameters toSessionSignatureAlgorithmParams() { + var mgfParams = new SessionMaskGenAlgorithmParameters(); + mgfParams.setHashAlgorithm("SHA-512"); + + var mgf = new SessionMaskGenAlgorithm(); + mgf.setAlgorithm("id-mgf1"); + mgf.setParameters(mgfParams); + + var params = new SessionSignatureAlgorithmParameters(); + params.setHashAlgorithm("SHA-512"); + params.setMaskGenAlgorithm(mgf); + params.setSaltLength(64); + params.setTrailerField("0xbc"); + return params; } } diff --git a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java index 49f900a2..2e9ec703 100644 --- a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -58,12 +58,12 @@ class SignatureValueValidatorImplTest { @BeforeEach void setUp() { - signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + signatureValueValidator = new SignatureValueValidatorImpl(); } @Test void validate() throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(CERT); + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(CERT); RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); @@ -81,7 +81,7 @@ void validateSignatureValue_IsInvalid_throwException() { () -> signatureValueValidator.validate( "invalidValue".getBytes(StandardCharsets.UTF_8), PAYLOAD, - CertificateUtil.getX509Certificate(CERT), + CertificateUtil.toX509CertificateFromEncodedString(CERT), toRsaSsaPssParameters())); assertEquals("Signature value validation failed", ex.getMessage()); } @@ -92,7 +92,7 @@ void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwExce () -> signatureValueValidator.validate( SIGNATURE_VALUE, "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), - CertificateUtil.getX509Certificate(CERT), + CertificateUtil.toX509CertificateFromEncodedString(CERT), toRsaSsaPssParameters())); assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); } @@ -114,7 +114,7 @@ public Stream provideArguments(ExtensionContext context) th Arguments.of(null, null, null, null), Arguments.of(new byte[0], null, null, null), Arguments.of(new byte[0], new byte[0], null, null), - Arguments.of(new byte[0], new byte[0], CertificateUtil.getX509Certificate(CERT), null) + Arguments.of(new byte[0], new byte[0], CertificateUtil.toX509CertificateFromEncodedString(CERT), null) ); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 91c0869d..bc558be9 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -47,6 +47,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.HashAlgorithm; @@ -60,6 +61,9 @@ class SmartIdClientTest { private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String PERSON_CODE = "PNOEE-1234567890"; + private static final String INITIAL_CALLBACK_URL = "https://example.com/callback"; private SmartIdClient smartIdClient; @@ -77,22 +81,58 @@ void setUp() { class DeviceLinkCertificateChoiceSession { @Test - void createDeviceLinkCertificateChoice() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + void createSameDeviceCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createSameDeviceCertificateChoiceSessionWithAllFields() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .withNonce("d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk") + .withShareMdClientIpAddress(true) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createQrCodeCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) - .withInitialCallbackUrl("https://smart-id.com/device-link/") .initCertificateChoice(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getDeviceLinkBase()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } } @@ -102,12 +142,14 @@ class NotificationCertificateChoiceSession { @Test void createNotificationCertificateChoice_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", "requests/certificate-choice-session-request.json", "responses/notification-certificate-choice-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/certificate-choice-session-request.json", + "responses/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .initCertificateChoice(); assertNotNull(response.getSessionID()); @@ -120,7 +162,9 @@ class DeviceLinkAuthenticationSession { @Test void createDeviceLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) @@ -128,44 +172,51 @@ void createDeviceLinkAuthentication_anonymous() { .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } @Test void createDeviceLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } @Test void createDeviceLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .initAuthenticationSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } } @@ -175,48 +226,52 @@ class DeviceLinkSignatureSession { @Test void createDeviceLinkSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/device-link-signature-request.json", "responses/device-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackUrl("https://example.com/callback") + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getDeviceLinkBase()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } @Test void createDeviceLinkSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", "requests/device-link-signature-request.json", "responses/device-link-signature-session-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); signableHash.setHashType(HashType.SHA512); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackUrl("https://example.com/callback") + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getSessionToken()); - assertNotNull(response.getSessionSecret()); - assertNotNull(response.getDeviceLinkBase()); - assertNotNull(response.getReceivedAt()); + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); } } @@ -226,10 +281,12 @@ class CertificateByDocumentNumberRequest { @Test void createCertificateRequest_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response.json"); CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withCertificateLevel(CertificateLevel.ADVANCED) .getCertificateByDocumentNumber(); @@ -240,10 +297,12 @@ void createCertificateRequest_withDocumentNumber() { @Test void getCertificateByDocumentNumber_withUnknownState_throwsException() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response-unknown-state.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response-unknown-state.json"); CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withCertificateLevel(CertificateLevel.ADVANCED); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); @@ -259,10 +318,12 @@ class NotificationAuthenticationSession { @Test void createNotificationAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", + "requests/auth/notification/notification-authentication-session-request.json", + "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession(); @@ -275,10 +336,12 @@ void createNotificationAuthentication_withSemanticsIdentifier() { @Test void createNotificationAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/auth/notification/notification-authentication-session-request.json", + "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .initAuthenticationSession(); @@ -294,16 +357,19 @@ void createNotificationAuthentication_withDocumentNumber() { @Nested @WireMockTest(httpPort = 18089) class NotificationBasedSignatureSession { + @Test void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/notification/notification-signature-session-request.json", + "responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); signableHash.setHashType(HashType.SHA512); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withDocumentNumber(DOCUMENT_NUMBER) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) @@ -317,7 +383,9 @@ void createNotificationSignature_withDocumentNumber() { @Test void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/notification-signature-session-request.json", + "responses/notification-session-response.json"); var signableHash = new SignableHash(); signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); @@ -326,7 +394,7 @@ void createNotificationSignature_withSemanticsIdentifier() { NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) .withSignableHash(signableHash) @@ -366,149 +434,307 @@ void getSessionStatus() { @Nested @WireMockTest(httpPort = 18089) - class DynamicContent { + class DynamicContentForAuth { @ParameterizedTest @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_authenticationWithWeb2AppAndApp2App(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .initAuthenticationSession(); + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + AuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - URI qrCodeUri = new DeviceLinkBuilder() + URI deviceLink = smartIdClient.createDynamicContent() .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + .withDeviceLinkBase(response.deviceLinkBase().toString()) .withDeviceLinkType(deviceLinkType) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.getSessionToken()) - .withRelyingPartyName("DEMO") + .withSessionToken(response.sessionToken()) .withLang("eng") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInitialCallbackUrl("https://smart-id.com/callback") - .buildDeviceLink(response.getSessionSecret()); + .withInitialCallbackUrl(request.initialCallbackUrl()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); - assertUri(qrCodeUri, SessionType.AUTHENTICATION, deviceLinkType, response.getSessionToken()); + assertUri(deviceLink, SessionType.AUTHENTICATION, deviceLinkType, response.sessionToken()); } @Test void createDynamicContent_authenticationWithQRCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .initAuthenticationSession(); + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI qrCodeUri = new DeviceLinkBuilder() + URI qrCodeUri = smartIdClient.createDynamicContent() .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + .withDeviceLinkBase(response.deviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.getSessionToken()) + .withSessionToken(response.sessionToken()) .withElapsedSeconds(elapsedSeconds) - .withRelyingPartyName("DEMO") .withLang("eng") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .buildDeviceLink(response.getSessionSecret()); + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); - assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.getSessionToken()); + assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); } @Test - void createDynamicContent_certificateChoiceWithWithQRCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); + void createDynamicContent_authenticationWithQRCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/device-link-authentication-session-request.json", + "responses/device-link-authentication-session-response.json"); - long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - URI fullUri = new DeviceLinkBuilder() - .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.getSessionToken()) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) .withElapsedSeconds(elapsedSeconds) .withLang("eng") - .withRelyingPartyName("DEMO") - .buildDeviceLink(response.getSessionSecret()); + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.getSessionToken()); + assertUri(uri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForSignature { @ParameterizedTest @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getHashInBase64()) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.SIGNATURE, deviceLinkType, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getHashInBase64()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getHashInBase64()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForCertificateChoice { + + @Test + void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); - URI fullUri = new DeviceLinkBuilder() - .withDeviceLinkBase(response.getDeviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.getSessionToken()) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) .withLang("eng") - .withRelyingPartyName("DEMO") - .withInitialCallbackUrl("https://smart-id.com/callback") - .buildDeviceLink(response.getSessionSecret()); + .buildDeviceLink(response.sessionSecret()); - assertUri(fullUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.getSessionToken()); + assertUri(deviceLink, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); } @Test - void createDynamicContent_createQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", "requests/certificate-choice-session-request.json", "responses/device-link-certificate-choice-session-response.json"); + void createDynamicContent_createQrCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) .withCertificateLevel(CertificateLevel.ADVANCED) .initCertificateChoice(); - long elapsedSeconds = Duration.between(response.getReceivedAt(), Instant.now()).getSeconds(); + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI fullUri = new DeviceLinkBuilder() - .withDeviceLinkBase(response.getDeviceLinkBase().toString()) + URI qrCodeUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.getSessionToken()) + .withSessionToken(response.sessionToken()) .withElapsedSeconds(elapsedSeconds) .withLang("eng") - .withRelyingPartyName("DEMO") - .buildDeviceLink(response.getSessionSecret()); + .buildDeviceLink(response.sessionSecret()); - String qrCodeDataUri = QrCodeGenerator.generateDataUri(fullUri.toString()); + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); String[] qrCodeDataUriParts = qrCodeDataUri.split(","); URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.getSessionToken()); + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); } - private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { - assertEquals("https", qrCodeUri.getScheme()); - assertEquals("smart-id.com", qrCodeUri.getHost()); - assertEquals("/device-link/", qrCodeUri.getPath()); - - assertTrue(qrCodeUri.getQuery().contains("version=1.0")); - assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); - assertTrue(qrCodeUri.getQuery().contains("lang=eng")); - assertTrue(qrCodeUri.getQuery().contains("authCode=")); + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + URI deviceLinkUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withInitialCallbackUrl("https://smart-id.com/callback") + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLinkUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.sessionToken()); } } + + private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { + assertEquals("https", qrCodeUri.getScheme()); + assertEquals("smart-id.com", qrCodeUri.getHost()); + assertEquals("/device-link/", qrCodeUri.getPath()); + + assertTrue(qrCodeUri.getQuery().contains("version=1.0")); + assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); + assertTrue(qrCodeUri.getQuery().contains("lang=eng")); + assertTrue(qrCodeUri.getQuery().contains("authCode=")); + } } diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 029fccf9..66b6118b 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -106,6 +106,16 @@ public static void stubRequestWithResponse(String url, String requestFile, Strin .withBody(readFileBody(responseFile)))); } + public static void stubStrictRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), false, false)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { String urlEquals = "/session/" + sessionId; stubFor(get(urlEqualTo(urlEquals)) diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 2390fbe4..f945eaa9 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -49,15 +49,13 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import ee.sk.smartid.AuthenticationCertificateLevel; import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.AuthenticationResponseValidator; import ee.sk.smartid.CertificateByDocumentNumberResult; import ee.sk.smartid.CertificateChoiceResponse; -import ee.sk.smartid.CertificateChoiceResponseMapper; +import ee.sk.smartid.CertificateChoiceResponseValidator; import ee.sk.smartid.CertificateLevel; import ee.sk.smartid.CertificateValidator; import ee.sk.smartid.CertificateValidatorImpl; @@ -93,7 +91,6 @@ public class ReadmeIntegrationTest { private static final String ALPHA_NUMERIC_PATTERN = "^[A-z0-9]{4}$"; - private static final Logger log = LoggerFactory.getLogger(ReadmeIntegrationTest.class); private SmartIdClient smartIdClient; @@ -138,14 +135,14 @@ void anonymousAuthentication_withApp2App() { AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.getSessionID(); + String sessionId = authenticationSessionResponse.sessionID(); // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.getSessionToken(); + String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Will be used to calculate elapsed time being used in dynamic link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse @@ -212,14 +209,14 @@ void authentication_withSemanticIdentifierAndQrCode() { AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.getSessionID(); + String sessionId = authenticationSessionResponse.sessionID(); // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.getSessionToken(); + String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Will be used to calculate elapsed time being used in dynamic link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse @@ -283,14 +280,14 @@ void authentication_withDocumentNumberAndQrCode() { // Get AuthenticationSessionRequest after the request is made and store for validations AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - String sessionId = authenticationSessionResponse.getSessionID(); + String sessionId = authenticationSessionResponse.sessionID(); // SessionID is used to query sessions status later - String sessionToken = authenticationSessionResponse.getSessionToken(); + String sessionToken = authenticationSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.getSessionSecret(); - Instant responseReceivedAt = authenticationSessionResponse.getReceivedAt(); - URI deviceLinkBase = authenticationSessionResponse.getDeviceLinkBase(); + String sessionSecret = authenticationSessionResponse.sessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); // Generate the base (unprotected) device link URI, which does not yet include the authCode long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); @@ -358,12 +355,12 @@ void signature_withDocumentNumberAndQRCode() { .initSignatureSession(); // Process the signature response - String signatureSessionId = signatureSessionResponse.getSessionID(); - String sessionToken = signatureSessionResponse.getSessionToken(); + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.getSessionSecret(); - Instant receivedAt = signatureSessionResponse.getReceivedAt(); - URI deviceLinkBase = signatureSessionResponse.getDeviceLinkBase(); + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); + URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse // Start querying sessions status @@ -396,15 +393,15 @@ void signature_withDocumentNumberAndQRCode() { CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); // Validate signature response - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); // Validate signature value - SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); assertNotNull(signatureResponse.getCertificate()); } @@ -432,7 +429,10 @@ void signature_withSemanticIdentifier() { // Querying the sessions status SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); // For example construct DataToSign using digidoc4j library and queried certificate // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); @@ -458,12 +458,12 @@ void signature_withSemanticIdentifier() { .initSignatureSession(); // Process the signature response - String signatureSessionId = signatureSessionResponse.getSessionID(); - String sessionToken = signatureSessionResponse.getSessionToken(); + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.getSessionSecret(); - Instant receivedAt = signatureSessionResponse.getReceivedAt(); + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse // Start querying sessions status @@ -489,14 +489,11 @@ void signature_withSemanticIdentifier() { // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) assertEquals("COMPLETE", signatureSessionStatus.getState()); - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - // Validate signature response SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); // Validate signature value - SignatureValueValidator signatureValueValidator = SignatureValueValidatorImpl.getInstance(); + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certificateChoiceResponse.getCertificate(), @@ -504,8 +501,8 @@ void signature_withSemanticIdentifier() { assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); assertNotNull(signatureResponse.getCertificate()); } @@ -635,7 +632,11 @@ void certificateChoice_withSemanticIdentifier() { // Querying the sessions status SessionStatus sessionStatus = poller.getSessionStatus(sessionId); - CertificateChoiceResponse response = CertificateChoiceResponseMapper.from(sessionStatus); + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); + assertEquals("OK", response.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); assertNotNull(response.getCertificate()); @@ -666,7 +667,10 @@ void signature_withSemanticsIdentifier() { // Querying the sessions status SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - CertificateChoiceResponse certificateChoiceResponse = CertificateChoiceResponseMapper.from(certificateSessionStatus); + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus); // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); // Create the signable data @@ -703,15 +707,13 @@ void signature_withSemanticsIdentifier() { // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) assertEquals("COMPLETE", signatureSessionStatus.getState()); - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); assertEquals("OK", signatureResponse.getEndResult()); assertEquals("PNOEE-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED.name(), signatureResponse.getRequestedCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); assertEquals("verificationCodeChoice", signatureResponse.getInteractionFlowUsed()); assertNotNull(signatureResponse.getCertificate()); } @@ -730,7 +732,7 @@ void queryCertificate() { .withDocumentNumber(documentNumber) .getCertificateByDocumentNumber(); - // Setup the certificate validator + // Set up the certificate validator TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 6b615d85..63684591 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -96,10 +96,10 @@ void initAnonymousDeviceLinkAuthentication() { DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); - assertNotNull(sessionsResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); + assertNotNull(sessionsResponse.receivedAt()); } @Test @@ -108,10 +108,10 @@ void initDeviceLinkAuthentication_withDocumentNumber() { DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); - assertNotNull(sessionsResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); + assertNotNull(sessionsResponse.receivedAt()); } @Test @@ -120,10 +120,10 @@ void initDeviceLinkAuthentication_withSemanticsIdentifier() { DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionResponse.getSessionSecret())); - assertNotNull(sessionResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionResponse.sessionSecret())); + assertNotNull(sessionResponse.receivedAt()); } private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { @@ -149,17 +149,23 @@ class CertificateChoice { @Test void initDeviceLinkCertificateChoice() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); - request.setRelyingPartyName(RELYING_PARTY_NAME); + var request = new CertificateChoiceSessionRequest( + RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + null, + null, + null, + null + ); DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); - assertNotNull(sessionsResponse.getDeviceLinkBase()); - assertNotNull(sessionsResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); + assertNotNull(sessionsResponse.deviceLinkBase()); + assertNotNull(sessionsResponse.receivedAt()); } } @@ -185,10 +191,10 @@ void initDeviceLinkSignature_withSemanticIdentifier() { DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); - assertNotNull(sessionsResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); + assertNotNull(sessionsResponse.receivedAt()); } @Test @@ -211,10 +217,10 @@ void initDeviceLinkSignature_withDocumentNumber() { DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.getSessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.getSessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.getSessionSecret())); - assertNotNull(sessionsResponse.getReceivedAt()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); + assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); + assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); + assertNotNull(sessionsResponse.receivedAt()); } } } @@ -271,9 +277,7 @@ class CertificateChoice { @Test void initNotificationCertificateChoice_withSemanticIdentifier() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyName(RELYING_PARTY_NAME); - request.setRelyingPartyUUID(RELYING_PARTY_UUID); + var request = new CertificateChoiceSessionRequest(RELYING_PARTY_NAME, RELYING_PARTY_UUID, null, null, null, null, null); NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, new SemanticsIdentifier("PNOEE-40504040001")); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 233487ca..e4aed58a 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -453,7 +453,7 @@ void setUp() { @Disabled("Request body has changed") @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); assertNotNull(response); @@ -462,7 +462,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -471,7 +471,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); }); } @@ -494,7 +494,7 @@ void setUp() { @Disabled("Request body has changed") @Test void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); assertNotNull(response); @@ -503,7 +503,7 @@ void initNotificationAuthentication() { @Test void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -512,7 +512,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { @Test void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/notification-authentication-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); }); } @@ -533,7 +533,7 @@ public void setUp() { @Test void initDeviceLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/device-link-certificate-choice-session-response.json"); + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); @@ -546,7 +546,6 @@ void initDeviceLinkCertificateChoice() { @Test void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - request.setCertificateLevel("INVALID_LEVEL"); stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); @@ -573,9 +572,7 @@ void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { void initDeviceLinkCertificateChoice_invalidRequest() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID(""); - request.setRelyingPartyName(""); + var request = new CertificateChoiceSessionRequest("", "", null, null, null, null, null); assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -628,6 +625,18 @@ void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } + + private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + return new CertificateChoiceSessionRequest( + "00000000-0000-0000-0000-000000000000", + "DEMO", + "ADVANCED", + null, + null, + null, + null + ); + } } @Nested @@ -660,7 +669,7 @@ void initCertificateChoice_withSemanticsIdentifier_successful() { @Test void initCertificateChoice_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } @@ -668,10 +677,22 @@ void initCertificateChoice_userAccountNotFound_throwException() { @Test void initCertificateChoice_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/certificate-choice-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/certificate-choice-session-request.json"); connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); }); } + + private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + return new CertificateChoiceSessionRequest( + "00000000-0000-0000-0000-000000000000", + "DEMO", + "ADVANCED", + "cmFuZG9tTm9uY2U=", + null, + null, + null + ); + } } @Nested @@ -747,7 +768,7 @@ public void setUp() { @Test void initDeviceLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/device-link-signature-session-response.json"); + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); @@ -755,15 +776,15 @@ void initDeviceLinkSignature_withSemanticsIdentifier_successful() { DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, semanticsIdentifier); assertNotNull(response); - assertEquals("test-session-id", response.getSessionID()); - assertEquals("test-session-token", response.getSessionToken()); - assertEquals("test-session-secret", response.getSessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.getDeviceLinkBase()); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); } @Test void initDeviceLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/device-link-signature-session-response.json"); + stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); SignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111-MOCK-Q"; @@ -771,10 +792,10 @@ void initDeviceLinkSignature_withDocumentNumber_successful() { DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); assertNotNull(response); - assertEquals("test-session-id", response.getSessionID()); - assertEquals("test-session-token", response.getSessionToken()); - assertEquals("test-session-secret", response.getSessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.getDeviceLinkBase()); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); } @Test @@ -879,7 +900,7 @@ void setUp() { @Disabled("Request body has changed") @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -895,7 +916,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -908,7 +929,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Disabled("Request body has changed") @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -987,7 +1008,7 @@ void setUp() { @Disabled("Request body has changed") @Test void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json", "responses/notification-session-response.json"); + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -1003,7 +1024,7 @@ void initNotificationSignature() { @Test void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -1016,7 +1037,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { @Disabled("Request body has changed") @Test void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/notification-signature-session-request.json"); + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json"); SignatureSessionRequest request = createNotificationSignatureSessionRequest(); @@ -1115,15 +1136,6 @@ private static AuthenticationSessionRequest toNotificationAuthenticationSessionR ); } - private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { - var request = new CertificateChoiceSessionRequest(); - request.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); - request.setRelyingPartyName("DEMO"); - request.setCertificateLevel("ADVANCED"); - request.setNonce("cmFuZG9tTm9uY2U="); - return request; - } - private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { return new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", "ADVANCED"); } @@ -1169,11 +1181,11 @@ private static void assertResponseValues(DeviceLinkSessionResponse response, Instant start, Instant end) { assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); - assertEquals(expectedSessionToken, response.getSessionToken()); - assertEquals(expectedSessionSecret, response.getSessionSecret()); - assertNotNull(response.getReceivedAt()); - assertFalse(response.getReceivedAt().isBefore(start.minusSeconds(1))); - assertFalse(response.getReceivedAt().isAfter(end.plusSeconds(1))); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + assertEquals(expectedSessionToken, response.sessionToken()); + assertEquals(expectedSessionSecret, response.sessionSecret()); + assertNotNull(response.receivedAt()); + assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); + assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); } } diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index 0efebf0d..b4102378 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -27,15 +27,18 @@ */ import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.LocalDate; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; @@ -49,6 +52,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.InvalidCertificateGenerator; public class CertificateAttributeUtilTest { @@ -57,7 +61,7 @@ public class CertificateAttributeUtilTest { @Test public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { - X509Certificate certificateWithDob = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); + X509Certificate certificateWithDob = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); @@ -67,7 +71,7 @@ public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws @Test public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { - X509Certificate certificateWithoutDobAttribute = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV); + X509Certificate certificateWithoutDobAttribute = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); @@ -77,7 +81,7 @@ public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() @ParameterizedTest @ArgumentsSource(AttributeArgumentProvider.class) void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); String distinguishedName = certificate.getSubjectX500Principal().getName(); Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); @@ -88,7 +92,7 @@ void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) thr @Test void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { - X509Certificate certificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_WITH_DOB); + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); String distinguishedName = certificate.getSubjectX500Principal().getName(); Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); @@ -96,6 +100,38 @@ void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws Certificat assertTrue(attributeValue.isEmpty()); } + @Test + void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertTrue(certificatePolicy.isEmpty()); + } + + @Test + void getCertificatePolicy_certificatePolicyPresent() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertThat(certificatePolicy, contains("1.3.6.1.4.1.10015.3.17.2", "0.4.0.2042.1.1")); + } + + @Test + void hasNonRepudiation_KeyUsageExtensionIsMissing() { + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + + assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); + } + + @Test + void hasNonRepudiation_KeyUsageExtensionIsMising() { + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + + assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); + } + private static class AttributeArgumentProvider implements ArgumentsProvider { @Override diff --git a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java index a3e9fdad..47e53d1f 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java @@ -42,8 +42,8 @@ import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.AuthenticationIdentityMapper; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; public class NationalIdentityNumberUtilTest { @@ -53,7 +53,7 @@ public class NationalIdentityNumberUtilTest { @Test public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { - X509Certificate eeCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_EE); + X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); @@ -65,7 +65,7 @@ public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws Certificate @Test public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { - X509Certificate lvCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); + X509Certificate lvCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); @@ -77,7 +77,7 @@ public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateE @Test public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { - X509Certificate ltCertificate = CertificateUtil.getX509Certificate(AUTH_CERTIFICATE_LT); + X509Certificate ltCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json new file mode 100644 index 00000000..38cfa286 --- /dev/null +++ b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json @@ -0,0 +1,10 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback", + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "requestProperties": { + "shareMdClientIpAddress": true + } +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json new file mode 100644 index 00000000..c9a48b43 --- /dev/null +++ b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json @@ -0,0 +1,6 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json new file mode 100644 index 00000000..88c2a4d0 --- /dev/null +++ b/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json @@ -0,0 +1,5 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json new file mode 100644 index 00000000..e19b9623 --- /dev/null +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json @@ -0,0 +1,14 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" +} \ No newline at end of file diff --git a/src/test/resources/requests/device-link-signature-request.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json similarity index 100% rename from src/test/resources/requests/device-link-signature-request.json rename to src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json new file mode 100644 index 00000000..e19b9623 --- /dev/null +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json @@ -0,0 +1,14 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" +} \ No newline at end of file diff --git a/src/test/resources/requests/certificate-choice-session-request.json b/src/test/resources/requests/sign/notification/certificate-choice-session-request.json similarity index 100% rename from src/test/resources/requests/certificate-choice-session-request.json rename to src/test/resources/requests/sign/notification/certificate-choice-session-request.json diff --git a/src/test/resources/requests/notification-signature-session-request.json b/src/test/resources/requests/sign/notification/notification-signature-session-request.json similarity index 100% rename from src/test/resources/requests/notification-signature-session-request.json rename to src/test/resources/requests/sign/notification/notification-signature-session-request.json diff --git a/src/test/resources/responses/device-link-certificate-choice-session-response.json b/src/test/resources/responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json similarity index 100% rename from src/test/resources/responses/device-link-certificate-choice-session-response.json rename to src/test/resources/responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json diff --git a/src/test/resources/responses/device-link-signature-session-response.json b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json similarity index 85% rename from src/test/resources/responses/device-link-signature-session-response.json rename to src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json index 7caaa454..ae3b7df6 100644 --- a/src/test/resources/responses/device-link-signature-session-response.json +++ b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json @@ -1,7 +1,7 @@ { "sessionID": "test-session-id", "sessionToken": "test-session-token", - "sessionSecret": "test-session-secret", + "sessionSecret": "c2Vzc2lvblNlY3JldA==", "deviceLinkBase": "https://smart-id.com/device-link/", "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/test-certs/nq-signing-cert.pem b/src/test/resources/test-certs/nq-signing-cert.pem new file mode 100644 index 00000000..d1aedc43 --- /dev/null +++ b/src/test/resources/test-certs/nq-signing-cert.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGdDCCBfmgAwIBAgIQayQDbT+81MKPikMhkxHiEjAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI1MDgzMTA1NTkyNVoXDTI4MDgzMDA1NTkyNFow +XzELMAkGA1UEBhMCRkkxFDASBgNVBAMMC01VU0VSLFVSTUFTMQ4wDAYDVQQEDAVN +VVNFUjEOMAwGA1UEKgwFVVJNQVMxGjAYBgNVBAUTEVBOT0ZJLTM5MDAzMDEyNzk4 +MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAmX7GLKF5e8LxXrRNl5sM +AHA3UwM9/RKIjAePphFJ1IwEu4f1NutqGWs9hhsh6PnBnbnKsvN/US//5Bw/t37z +gBqgQaToPCww9zHsUFl0w3Q+XGxK3wzKltu50WaxIQaLExFTgFsPmViabT7M1Kqu +6UYlJQJQPkgkfvmuz3LeCFXFRpjEoIgvVfbAYxXfn8V1HPwPAhFTkC1iTht14SOE +WolghzV5R3IYeCey8Y978rFy24avN+ea1aweti98UaFH8wIjuX1zND7SHY2fu5tF ++uxSacJuBUukL0w34n3ODEPDJXojPEnJEIOZmJIV35jGcTMEc6OVlKHYBfwISUjy ++/dIy+oq/qz7z5Kr4gv2DBTLpEj6bCgS2uDDxVXZbDFY3K0jXtxVtk11pWKFUEtd +YJFV6FRswFHF/WYp8o2WJ7O0hlB47pkUgLtKXLNJrS8z+dLcP3s626ZSjfEPf3LD +Ku9GoDetBzvj+QanNUpzZGpWdaMNN4dpLdMSyp2WK6KK3TXowmYYSaXS2t+5MyM9 +0Om3JUkY9hIBIfeAgGwA9Vx8OT5srYU1zWIGcP8AeTsE2JDpCh11M56dAw8yF2z0 +JyTjYcmmX/Zla3gT76yhL43AHT64hxohpiZr8R2lKSFO1qg7ECly1XKov5Vm8rTd +CaDiWLrBwh4+XEJSludG6KZ+0Q8hrGU23qNBH/Yo4xh2vArwdaoKpHUiMbwu9/MZ +k7WTgCqxnp9fC6l+25ePfK4xiA5iVfZsmBs8iE8Z/J3akd2tZRW3jE672F1L+V6m +icbVCi8VFpWcVfdTZFvQPoozZAx8dm//L4hEEeu/7ylEmrn4luEcMsk386Pj61KB +wk54zzuxWc9i3u4JyW3B6F87LhGv01osVK35mXWhBI5hN8YrRHieHafuMGrfdGp9 +XrKRleYtih2BYr+FD2bfaxf0KEBaQdHMXEZ814x3SYLb1iv7/5yz6FNB37OwlIwa +VvoxaIggGXI0Ht0axQ6dqvDdo1cH+uXNYZJtp+ScQ6qVAgMBAAGjggG3MIIBszAJ +BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIGCCsG +AQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9FSUQt +TlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1vLnNr +LmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0ZJLTM5 +MDAzMDEyNzk4LTlUMTctTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATA1BgNV +HR8ELjAsMCqgKKAmhiRodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5j +cmwwHQYDVR0OBBYEFMiBd4urSvDE0mt9B6CBjlPj8RMcMA4GA1UdDwEB/wQEAwIG +QDAKBggqhkjOPQQDAwNpADBmAjEA/dDkzO4iuKidm3IP4j+5JKOrzhn1+XO7WzbM +Uvu3+3Wn0zUAg88I0tAFPUHUsVqrAjEAzlmPXUdhTGEH7dGQowFHVnsSUP4o0Q/f +qqcQOidwglE0899fEZoQSLHJ6tR0K7ip +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates.jks b/src/test/resources/trusted_certificates.jks index d7f73f58b605364e712d945ed5a0452fc4330240..5074e4c4a2a1883b24115447d1d0374c93f888aa 100644 GIT binary patch delta 629 zcmcbs{7l>Q-`jt085kItfS3hLyO*REmnh_?DHLZbWTq$-=jW7`Waj4;E2L(o=;jqF z7#SEDPCm}7u0P|ywvrYDYlNPufh7Y2^Gbsz=A}T)wt$(5k%>vbPkDFw*3~Z}6MhKa zs_YPznXJgyC~Ih7Ze(a^WNc()7A4MWY=*=&D4ML#=fb6Hpv}e{%EHW}GP#ydg4@;8 zMb|G7VyQmAWIbEmY~cr2V}1BOv576w3pIWKAI~-$OBy=tIQ%{Al4xANakMhoqa!A>KG^A=I7Rv1xfL-h_Q%l zj=ZpSRrb%u4O`e3uRVNj>idshcsPx8%@d5Kg#iu_4nVp$Kt|3 zGwKw+G8-1_gx}9;T4kTSG$plbkzSVDCOPjTTux4NkIK)QH8s&QhFN^(S;+{6H{Zo4 x=qF5hz2)#1U)fKL3SXtJ={(rTvsX~WvisIlFW0>rS_02SEYnoT7x36~4FFjv-xvS@ delta 43 ycmaE+eOH<1-`jt085kItfS7r+D9c_!kw3-L^&5D^7e0TyhpBRN{s!f#{vQE&ml5j# From aeb21f64959f10c7ea6f2aed0a2503746ebca614 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 4 Sep 2025 12:58:03 +0300 Subject: [PATCH 38/57] Fix documentation of hash algorithm used as default for device signature request (#131) * SLIB-102 - change CertificateChoiceSessionRequest and DeviceLinkSessionResponse to records * SLIB-105 - fix incorrect documentation --- .../ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java | 2 +- src/test/java/ee/sk/smartid/SmartIdClientTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 3de562a3..93da09a6 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -155,7 +155,7 @@ public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capab /** * Sets the hash algorithm to be used for signature creation. - * By default, SHA3-512 is used. + * By default, SHA-512 is used. * * @param hashAlgorithm the hash algorithm to use * @return this builder diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index bc558be9..3b4a5506 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -658,7 +658,6 @@ void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { .initCertificateChoice(); long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() .withDeviceLinkBase(response.deviceLinkBase().toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) From b48e623f4adce11e071b9fc03742494b959afc0f Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Tue, 9 Sep 2025 15:19:34 +0300 Subject: [PATCH 39/57] Improve signature algorithm and document number validations in device signature request builder (#132) --- ...iceLinkSignatureSessionRequestBuilder.java | 29 ++++---- .../java/ee/sk/smartid/DigestCalculator.java | 26 +++++-- .../ee/sk/smartid/util/SignatureUtil.java | 19 ++++- ...inkSignatureSessionRequestBuilderTest.java | 71 ++++++++++++------- .../ee/sk/smartid/DigestCalculatorTest.java | 6 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 60 ++++++++++++++-- 6 files changed, 156 insertions(+), 55 deletions(-) diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 93da09a6..857f8866 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -46,6 +46,9 @@ import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; +/** + * Builder class for creating device link signature session + */ public class DeviceLinkSignatureSessionRequestBuilder { private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; @@ -250,11 +253,12 @@ public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String in * * @return a {@link DeviceLinkSessionResponse} containing session details such as * session ID, session token, session secret and device link base URL. - * @throws SmartIdClientException if request parameters are invalid + * @throws SmartIdRequestSetupException if request parameters are invalid + * @throws SmartIdClientException if digest calculation fails or if both signableData and signableHash are missing * @throws UnprocessableSmartIdResponseException if the response is missing required fields */ public DeviceLinkSessionResponse initSignatureSession() { - validateParameters(); + validateRequestParameters(); SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); validateResponseParameters(deviceLinkSignatureSessionResponse); @@ -262,12 +266,12 @@ public DeviceLinkSessionResponse initSignatureSession() { } private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { - if (documentNumber != null) { + if (!StringUtil.isEmpty(documentNumber)) { return connector.initDeviceLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { return connector.initDeviceLinkSignature(request, semanticsIdentifier); } else { - throw new SmartIdClientException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed."); + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed."); } } @@ -288,18 +292,21 @@ private SignatureSessionRequest createSignatureSessionRequest() { initialCallbackUrl); } - private void validateParameters() { - if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } - if (relyingPartyName == null || relyingPartyName.isEmpty()) { + if (StringUtil.isEmpty(relyingPartyName)) { throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } validateInteractions(); validateInitialCallbackUrl(); if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdClientException("Value for 'nonce' length must be between 1 and 30 characters."); + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters."); } } @@ -313,7 +320,7 @@ private void validateInteractions() { private void validateInitialCallbackUrl() { if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdClientException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); } } @@ -321,11 +328,9 @@ private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSign if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); } @@ -336,7 +341,7 @@ private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSign private void validateNoDuplicateInteractions() { if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { - throw new SmartIdClientException("Value for 'interactions' cannot contain duplicate types"); + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); } } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java index f02f9978..13ad0ada 100644 --- a/src/main/java/ee/sk/smartid/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/DigestCalculator.java @@ -27,18 +27,36 @@ */ import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; -public class DigestCalculator { +/** + * Utility class for calculating digests using specified hash algorithms. + */ +public final class DigestCalculator { + + private DigestCalculator() { + } + /** + * Calculates the digest of the provided data using the specified hash type. + * + * @param dataToDigest The data to be hashed. + * @param hashType The hash algorithm to use. + * @return The calculated digest as a byte array. + * @throws UnprocessableSmartIdResponseException If there is an issue with the digest calculation. + */ public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { + if (hashType == null) { + throw new SmartIdClientException("Parameter 'hashType' must be set"); + } try { MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); return digest.digest(dataToDigest); - } catch (Exception e) { - throw new UnprocessableSmartIdResponseException("Problem with digest calculation. " + e); + } catch (NoSuchAlgorithmException ex) { + throw new SmartIdClientException("Problem with digest calculation.", ex); } } - } diff --git a/src/main/java/ee/sk/smartid/util/SignatureUtil.java b/src/main/java/ee/sk/smartid/util/SignatureUtil.java index ab1017b1..80e39be6 100644 --- a/src/main/java/ee/sk/smartid/util/SignatureUtil.java +++ b/src/main/java/ee/sk/smartid/util/SignatureUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,8 +30,21 @@ import ee.sk.smartid.SignableHash; import ee.sk.smartid.exception.permanent.SmartIdClientException; -public class SignatureUtil { +/** + * Utility class for signature-related operations. + */ +public final class SignatureUtil { + + private SignatureUtil() { + } + /** + * Decides which of the two parameters to use for obtaining the Base64-encoded digest to sign. + * + * @param signableHash value to be sent for signing on data that was already hashed + * @param signableData value to be hashed before sending for signing + * @return Base64-encoded digest + */ public static String getDigestToSignBase64(SignableHash signableHash, SignableData signableData) { if (signableHash != null && signableHash.areFieldsFilled()) { return signableHash.getHashInBase64(); diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 0ba350b2..2d2724cd 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -57,6 +57,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; @@ -87,7 +88,6 @@ void setUp() { void initSignatureSession_withSemanticsIdentifier() { var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); builder.withSemanticsIdentifier(semanticsIdentifier); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -129,7 +129,6 @@ void initSignatureSession_withDocumentNumber() { @ArgumentsSource(CertificateLevelArgumentProvider.class) void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -148,7 +147,6 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel @ArgumentsSource(ValidNonceArgumentSourceProvider.class) void initSignatureSession_withNonce_ok(String nonce) { builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -166,7 +164,6 @@ void initSignatureSession_withNonce_ok(String nonce) { @Test void initSignatureSession_withRequestProperties() { builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -186,8 +183,9 @@ void initSignatureSession_withRequestProperties() { void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { var signableData = new SignableData("Test data".getBytes()); signableData.setHashType(HashType.SHA256); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - + builder.withSignableData(signableData) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -209,8 +207,9 @@ void initSignatureSession_withSignableHash(HashType hashType) { var signableHash = new SignableHash(); signableHash.setHash("Test hash".getBytes()); signableHash.setHashType(hashType); - builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - + builder.withSignableData(null) + .withSignableHash(signableHash) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -228,8 +227,8 @@ void initSignatureSession_withSignableHash(HashType hashType) { @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { - builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - + builder.withCapabilities(capabilities) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse signature = builder.initSignatureSession(); @@ -248,8 +247,8 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { var signableData = new SignableData("Test data".getBytes()); signableData.setHashType(HashType.SHA512); - builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - + builder.withSignableData(signableData) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(mockSignatureSessionResponse()); @@ -266,14 +265,28 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { @Nested class ErrorCases { - @Test - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { - builder.withDocumentNumber(null).withSemanticsIdentifier(null); + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { + builder.withDocumentNumber(documentNumber).withSemanticsIdentifier(null); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed.", ex.getMessage()); } + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var signableData = new SignableData("Test data".getBytes()); + signableData.setHashType(HashType.SHA256); + builder.withSignableData(signableData) + .withSignatureAlgorithm(null) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + @Test void initSignatureSession_whenHashTypeIsNull_throwsException() { var signableData = new SignableData("Test data".getBytes()); @@ -286,9 +299,9 @@ void initSignatureSession_whenHashTypeIsNull_throwsException() { @Test void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlgorithm() { - builder.withSignableHash(null).withSignableData(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenThrow(new SmartIdClientException("Either signableHash or signableData must be set.")); + builder.withSignableHash(null) + .withSignableData(null) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); @@ -299,14 +312,14 @@ void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlg void initSignatureSession_whenInteractionsIsNullOrEmpty(List interactions) { builder.withInteractions(interactions); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -324,7 +337,7 @@ void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) @ParameterizedTest @ArgumentsSource(DuplicateInteractionsProvider.class) void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { - var exception = assertThrows(SmartIdClientException.class, () -> + var exception = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -343,7 +356,7 @@ void initSignatureSession_duplicateInteractions_shouldThrowException(List builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } @@ -352,28 +365,32 @@ void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { builder.withRelyingPartyName(relyingPartyName); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } @Test void initSignatureSession_invalidNonce() { builder.withNonce("1234567890123456789012345678901"); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); } @Test void initSignatureSession_emptyNonce() { builder.withNonce(""); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); + + var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); } @Test void initSignatureSession_whenSignableHashNotFilled() { var signableHash = new SignableHash(); - builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + builder.withSignableData(null) + .withSignableHash(signableHash) + .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index eed4a309..a5052380 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -28,6 +28,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; @@ -35,7 +36,7 @@ import org.apache.commons.codec.binary.Hex; import org.junit.jupiter.api.Test; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; public class DigestCalculatorTest { @@ -67,6 +68,7 @@ public void calculateDigest_sha512() { @Test public void calculateDigest_nullHashType() { - assertThrows(UnprocessableSmartIdResponseException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); + var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); + assertEquals("Parameter 'hashType' must be set", ex.getMessage()); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 3b4a5506..539bad03 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -225,7 +225,7 @@ void createDeviceLinkAuthentication_withSemanticsIdentifier() { class DeviceLinkSignatureSession { @Test - void createDeviceLinkSignature_withDocumentNumber() { + void createDeviceLinkSignature_withDocumentNumberSameDevice() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); @@ -250,7 +250,31 @@ void createDeviceLinkSignature_withDocumentNumber() { } @Test - void createDeviceLinkSignature_withSemanticsIdentifier() { + void createDeviceLinkSignature_withDocumentNumberQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); @@ -273,6 +297,30 @@ void createDeviceLinkSignature_withSemanticsIdentifier() { assertNotNull(response.deviceLinkBase()); assertNotNull(response.receivedAt()); } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash(); + signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); + signableHash.setHashType(HashType.SHA512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } } @Nested @@ -540,7 +588,7 @@ class DynamicContentForSignature { @ParameterizedTest @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); @@ -572,7 +620,7 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { @Test void createDynamicContent_withQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); @@ -585,7 +633,6 @@ void createDynamicContent_withQrCode() { .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); @@ -606,7 +653,7 @@ void createDynamicContent_withQrCode() { @Test void createDynamicContent_withQrCodeImage() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); @@ -619,7 +666,6 @@ void createDynamicContent_withQrCodeImage() { .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); From ee64075ab860347c5f21c22b09c3930f7d3a5530 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 10 Sep 2025 15:20:43 +0300 Subject: [PATCH 40/57] Improve hash algorithm usage (#133) * SLIB-133 - remove duplicated HashAlgorithm; remove HashType; update SignableData and SignableHash to use HashAlgorithm * SLIB-133 - improve code quality and documentation * SLIB-133 - improve DeviceLinkSignatureSessionRequestBuilder tests * SLIB-133 - improve code style --- CHANGELOG.md | 3 + README.md | 22 +- .../ee/sk/smartid/AuthenticationHash.java | 82 +++-- .../AuthenticationResponseValidator.java | 3 +- ...nkAuthenticationSessionRequestBuilder.java | 5 +- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 1 + ...iceLinkSignatureSessionRequestBuilder.java | 37 +-- .../java/ee/sk/smartid/DigestCalculator.java | 23 +- .../HashAlgorithm.java => DigestInput.java} | 26 +- src/main/java/ee/sk/smartid/HashType.java | 56 ---- ...icationSignatureSessionRequestBuilder.java | 15 +- src/main/java/ee/sk/smartid/SignableData.java | 69 ++-- src/main/java/ee/sk/smartid/SignableHash.java | 64 ++-- .../smartid/VerificationCodeCalculator.java | 2 +- .../ee/sk/smartid/util/SignatureUtil.java | 60 ---- ...thenticationSessionRequestBuilderTest.java | 3 +- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 2 +- ...inkSignatureSessionRequestBuilderTest.java | 310 ++++++++---------- .../ee/sk/smartid/DigestCalculatorTest.java | 49 +-- ...ionSignatureSessionRequestBuilderTest.java | 29 +- .../java/ee/sk/smartid/SignatureUtilTest.java | 94 ------ .../java/ee/sk/smartid/SmartIdClientTest.java | 54 +-- .../VerificationCodeCalculatorTest.java | 2 +- .../integration/ReadmeIntegrationTest.java | 24 +- .../SmartIdRestIntegrationTest.java | 19 +- .../rest/SmartIdRestConnectorTest.java | 4 +- 26 files changed, 397 insertions(+), 661 deletions(-) rename src/main/java/ee/sk/smartid/{rest/dao/HashAlgorithm.java => DigestInput.java} (70%) delete mode 100644 src/main/java/ee/sk/smartid/HashType.java delete mode 100644 src/main/java/ee/sk/smartid/util/SignatureUtil.java delete mode 100644 src/test/java/ee/sk/smartid/SignatureUtilTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ab6a5b..27d094f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.12] - 2025-09-08 +- Removed HashType and update SignableHash and SignableData to use HashAlgorithm + ## [3.1.11] - 2025-08-25 - Updated CertificateChoiceResponseMapper - Renamed to CertificateChoiceResponseValidator diff --git a/README.md b/README.md index 1c7332b3..45401a03 100644 --- a/README.md +++ b/README.md @@ -409,8 +409,7 @@ The response from a successful device-link signature session creation contains t ```java // Create the signable data -var signableData = new SignableData("Test data to sign".getBytes()); -signableData.setHashType(HashType.SHA256); +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); // Create the Semantics Identifier var semanticsIdentifier = new SemanticsIdentifier( @@ -447,8 +446,7 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ```java // Create the signable data -var signableData = new SignableData("Test data to sign".getBytes()); -signableData.setHashType(HashType.SHA256); +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); // Specify the document number String documentNumber = "PNOEE-40504040001-MOCK-Q"; @@ -921,17 +919,17 @@ Checkout out other ways to set up TrustedCaCertStore with CertificateValidator i ## Notification-based flows -### Differences between notification-based and dynamic-link flows +### Differences between notification-based and device link flows * `Notification-Based flow` * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. * Known users or devices: * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using dynamic-link flows. * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. -* `Dynamic Link flow` - * Dynamic links: Generates links like QR codes or Web2App/App2App links that the user interacts with to start the process. - * Supports unknown users or devices: Useful when the user's identity or device is not known in advance. - * Real-time updates: Dynamic links and QR-code need to be refreshed every second to ensure validity. +* `Device Link flow` + * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. + * Authentication and certificate-choice support unknown users or devices: Useful when the user's identity or device is not known in advance. + * Real-time updates: QR-code needs to be refreshed every second to ensure validity. ### Notification-based authentication session @@ -1092,8 +1090,7 @@ The request parameters for the notification-based signature session are as follo ```java // Create the signable data -SignableData signableData = new SignableData("Data to sign".getBytes()); -signableData.setHashType(HashType.SHA256); +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); // Create the Semantics Identifier SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( @@ -1125,8 +1122,7 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ```java // Create the signable data -SignableData signableData = new SignableData("Data to sign".getBytes()); -signableData.setHashType(HashType.SHA256); +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); // Specify the document number String documentNumber = "PNOEE-40504040001-MOCK-Q"; diff --git a/src/main/java/ee/sk/smartid/AuthenticationHash.java b/src/main/java/ee/sk/smartid/AuthenticationHash.java index 3323a5e9..10dbd070 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationHash.java +++ b/src/main/java/ee/sk/smartid/AuthenticationHash.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,38 +31,56 @@ /** * Class containing the hash and its hash type used for authentication */ -public class AuthenticationHash extends SignableHash { +public class AuthenticationHash { - /** - * creates {@link AuthenticationHash} instance - * containing a randomly generated hash - * of the chosen hash type - * - * @param hashType hash type of the randomly generated hash - * @return authentication hash - */ - public static AuthenticationHash generateRandomHash(HashType hashType) { - AuthenticationHash authenticationHash = new AuthenticationHash(); - byte[] generatedDigest = DigestCalculator.calculateDigest(getRandomBytes(), hashType); - authenticationHash.setHash(generatedDigest); - authenticationHash.setHashType(hashType); - return authenticationHash; - } + private byte[] hash; + private HashAlgorithm hashAlgorithm; - /** - * creates {@link AuthenticationHash} instance - * containing a randomly generated SHA-512 hash - * - * @return authentication hash - */ - public static AuthenticationHash generateRandomHash() { - return generateRandomHash(HashType.SHA512); - } + /** + * creates {@link AuthenticationHash} instance + * containing a randomly generated hash + * of the chosen hash type + * + * @param hashAlgorithm hash type of the randomly generated hash + * @return authentication hash + */ + public static AuthenticationHash generateRandomHash(HashAlgorithm hashAlgorithm) { + AuthenticationHash authenticationHash = new AuthenticationHash(); + byte[] generatedDigest = DigestCalculator.calculateDigest(getRandomBytes(), hashAlgorithm); + authenticationHash.setHash(generatedDigest); + authenticationHash.setHashAlgorithm(hashAlgorithm); + return authenticationHash; + } - private static byte[] getRandomBytes() { - byte[] randBytes = new byte[64]; - new SecureRandom().nextBytes(randBytes); - return randBytes; - } + /** + * creates {@link AuthenticationHash} instance + * containing a randomly generated SHA-512 hash + * + * @return authentication hash + */ + public static AuthenticationHash generateRandomHash() { + return generateRandomHash(HashAlgorithm.SHA_512); + } + private static byte[] getRandomBytes() { + byte[] randBytes = new byte[64]; + new SecureRandom().nextBytes(randBytes); + return randBytes; + } + + public byte[] getHash() { + return hash; + } + + public void setHash(byte[] hash) { + this.hash = hash; + } + + public HashAlgorithm getHashAlgorithm() { + return hashAlgorithm; + } + + public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index ee6c42d5..1d5fc7ff 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -205,6 +205,7 @@ private static void validateInputs(SessionStatus sessionStatus, AuthenticationSe } private static byte[] calculateInteractionsDigest(AuthenticationSessionRequest authenticationSessionRequest) { - return DigestCalculator.calculateDigest(authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8), HashType.SHA256); + byte[] interactions = authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8); + return DigestCalculator.calculateDigest(interactions, HashAlgorithm.SHA_256); } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index ac4f5e43..dbb76764 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -38,7 +38,6 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; @@ -331,7 +330,7 @@ private void validateInitialCallbackUrl() { private AuthenticationSessionRequest createAuthenticationRequest() { var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(this.hashAlgorithm.getValue())); + new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); return new AuthenticationSessionRequest( relyingPartyUUID, diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 01e3a71e..2555c804 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -340,6 +340,7 @@ private void validateAuthCodeParams(String unprotectedLink) { if (sessionType != SessionType.CERTIFICATE_CHOICE && StringUtil.isEmpty(digest)) { throw new SmartIdClientException("digest must be set for AUTH or SIGN flows"); } + // TODO - 07.09.25: add interactions validation when only for certificate choice case should not be provided, otherwise required if (StringUtil.isEmpty(unprotectedLink)) { throw new SmartIdClientException("unprotected device-link must not be empty"); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 857f8866..25b19b5d 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -35,7 +35,6 @@ import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; @@ -43,7 +42,6 @@ import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.DeviceLinkUtil; -import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; /** @@ -62,13 +60,11 @@ public class DeviceLinkSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA_512; private List interactions; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private SignableData signableData; - private SignableHash signableHash; private String initialCallbackUrl; + private DigestInput digestInput; /** * Constructs a new Smart-ID signature request builder with the given connector. @@ -156,18 +152,6 @@ public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capab return this; } - /** - * Sets the hash algorithm to be used for signature creation. - * By default, SHA-512 is used. - * - * @param hashAlgorithm the hash algorithm to use - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return this; - } - /** * Sets the interactions for device-link signature. * @@ -205,13 +189,15 @@ public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(Signature * Sets the data to be signed. *

    * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - * If both {@link SignableData} and {@link SignableHash} are provided, {@link SignableData} will take precedence. * * @param signableData the data to be signed * @return this builder instance */ public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - this.signableData = signableData; + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash."); + } + this.digestInput = signableData; return this; } @@ -225,7 +211,10 @@ public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData si * @return this builder */ public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - this.signableHash = signableHash; + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData."); + } + this.digestInput = signableHash; return this; } @@ -276,10 +265,9 @@ private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest r } private SignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - SignatureUtil.getDigestToSignBase64(signableHash, signableData), + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(hashAlgorithm.getValue())); + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); return new SignatureSessionRequest(relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, @@ -302,6 +290,9 @@ private void validateRequestParameters() { if (signatureAlgorithm == null) { throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } validateInteractions(); validateInitialCallbackUrl(); diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java index 13ad0ada..a8ace55e 100644 --- a/src/main/java/ee/sk/smartid/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/DigestCalculator.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,7 +29,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; /** @@ -41,19 +40,19 @@ private DigestCalculator() { } /** - * Calculates the digest of the provided data using the specified hash type. + * Calculates the digest of the provided data using the specified hash algorithm. * - * @param dataToDigest The data to be hashed. - * @param hashType The hash algorithm to use. + * @param dataToDigest The data to be hashed. + * @param hashAlgorithm The hash algorithm to use. * @return The calculated digest as a byte array. - * @throws UnprocessableSmartIdResponseException If there is an issue with the digest calculation. + * @throws SmartIdClientException If there is an issue with the digest calculation. */ - public static byte[] calculateDigest(byte[] dataToDigest, HashType hashType) { - if (hashType == null) { - throw new SmartIdClientException("Parameter 'hashType' must be set"); + public static byte[] calculateDigest(byte[] dataToDigest, HashAlgorithm hashAlgorithm) { + if (hashAlgorithm == null) { + throw new SmartIdClientException("Parameter 'hashAlgorithm' must be set"); } try { - MessageDigest digest = MessageDigest.getInstance(hashType.getAlgorithmName()); + MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName()); return digest.digest(dataToDigest); } catch (NoSuchAlgorithmException ex) { throw new SmartIdClientException("Problem with digest calculation.", ex); diff --git a/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java b/src/main/java/ee/sk/smartid/DigestInput.java similarity index 70% rename from src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java rename to src/main/java/ee/sk/smartid/DigestInput.java index d964371f..afa6d8a8 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/HashAlgorithm.java +++ b/src/main/java/ee/sk/smartid/DigestInput.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid; /*- * #%L @@ -26,27 +26,9 @@ * #L% */ -import java.io.Serializable; +public interface DigestInput { -import com.fasterxml.jackson.annotation.JsonValue; + String getDigestInBase64(); -public enum HashAlgorithm implements Serializable { - - SHA_256("SHA-256"), - SHA_384("SHA-384"), - SHA_512("SHA-512"), - SHA3_256("SHA3-256"), - SHA3_384("SHA3-384"), - SHA3_512("SHA3-512"); - - private final String value; - - HashAlgorithm(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } + HashAlgorithm hashAlgorithm(); } diff --git a/src/main/java/ee/sk/smartid/HashType.java b/src/main/java/ee/sk/smartid/HashType.java deleted file mode 100644 index 426c7f44..00000000 --- a/src/main/java/ee/sk/smartid/HashType.java +++ /dev/null @@ -1,56 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -public enum HashType { - - SHA256("SHA-256", "SHA256", new byte[] { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 }), - SHA384("SHA-384", "SHA384", new byte[] { 0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30 }), - SHA512("SHA-512", "SHA512", new byte[] { 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40 }); - - private final String algorithmName; - private final String hashTypeName; - private final byte[] digestInfoPrefix; - - HashType(String algorithmName, String hashTypeName, byte[] digestInfoPrefix) { - this.algorithmName = algorithmName; - this.hashTypeName = hashTypeName; - this.digestInfoPrefix = digestInfoPrefix.clone(); - } - - public String getAlgorithmName() { - return algorithmName; - } - - public String getHashTypeName() { - return hashTypeName; - } - - public byte[] getDigestInfoPrefix() { - return digestInfoPrefix.clone(); - } -} diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 77b37953..3eeb2f76 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -35,7 +35,6 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -46,7 +45,6 @@ import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; import ee.sk.smartid.util.NotificationUtil; -import ee.sk.smartid.util.SignatureUtil; import ee.sk.smartid.util.StringUtil; public class NotificationSignatureSessionRequestBuilder { @@ -65,9 +63,7 @@ public class NotificationSignatureSessionRequestBuilder { private List allowedInteractionsOrder; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA_512; - private SignableData signableData; - private SignableHash signableHash; + private DigestInput digestInput; /** * Constructs a new Smart-ID signature request builder with the given connector. @@ -198,7 +194,7 @@ public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(Signatu * @return this builder instance */ public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - this.signableData = signableData; + this.digestInput = signableData; return this; } @@ -212,7 +208,7 @@ public NotificationSignatureSessionRequestBuilder withSignableData(SignableData * @return this builder */ public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - this.signableHash = signableHash; + this.digestInput = signableHash; return this; } @@ -247,10 +243,9 @@ private NotificationSignatureSessionResponse initSignatureSession(SignatureSessi } private SignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - SignatureUtil.getDigestToSignBase64(signableHash, signableData), + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(hashAlgorithm.getValue())); + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); return new SignatureSessionRequest(relyingPartyUUID, relyingPartyName, diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java index 40c4c2f1..29a7495a 100644 --- a/src/main/java/ee/sk/smartid/SignableData.java +++ b/src/main/java/ee/sk/smartid/SignableData.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,44 +29,65 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + /** * This class can be used to contain the data * to be signed when it is not yet in hashed format *

    - * {@link #setHashType(HashType)} can be used - * to set the wanted hash type. SHA-512 is default. - *

    - * {@link #calculateHash()} and - * {@link #calculateHashInBase64()} methods - * are used to calculate the hash for signing request. - *

    * {@link SignableHash} can be used * instead when the data to be signed is already * in hashed format. */ -public class SignableData implements Serializable { - - private final byte[] dataToSign; - private HashType hashType = HashType.SHA512; +public record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { + /** + * Creates a new instance of SignableData + *

    + * Will use SHA-512 as the default hashing algorithm + * + * @param dataToSign byte array of data to be signed + */ public SignableData(byte[] dataToSign) { + this(dataToSign, HashAlgorithm.SHA_512); + } + + /** + * Creates a new instance of SignableData + * + * @param dataToSign byte array of data to be signed + * @param hashAlgorithm hashing algorithm to be used + * @throws SmartIdRequestSetupException when input values are missing or empty + */ + public SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) { + if (dataToSign == null || dataToSign.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'dataToSign' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } this.dataToSign = dataToSign.clone(); + this.hashAlgorithm = hashAlgorithm; } - public String calculateHashInBase64() { + /** + * Calculates the digest of the data to be signed + * and returns it in Base64 encoded format + * + * @return Base64 encoded hash + */ + @Override + public String getDigestInBase64() { byte[] digest = calculateHash(); return Base64.getEncoder().encodeToString(digest); } + /** + * Calculates the digest of the data to be signed + * + * @return hash + */ public byte[] calculateHash() { - return DigestCalculator.calculateDigest(dataToSign, hashType); - } - - public void setHashType(HashType hashType) { - this.hashType = hashType; - } - - public HashType getHashType() { - return hashType; + return DigestCalculator.calculateDigest(dataToSign, hashAlgorithm); } } diff --git a/src/main/java/ee/sk/smartid/SignableHash.java b/src/main/java/ee/sk/smartid/SignableHash.java index 583e4094..9d18117a 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/SignableHash.java @@ -29,45 +29,59 @@ import java.io.Serializable; import java.util.Base64; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + /** * This class can be used to contain the hash * to be signed *

    - * {@link #setHash(byte[])} can be used - * to set the hash. - * {@link #setHashType(HashType)} can be used - * to set the hash type. - *

    * {@link SignableData} can be used * instead when the data to be signed is not already * in hashed format. */ -public class SignableHash implements Serializable { - - private byte[] hash; - private HashType hashType; - - public void setHash(byte[] hash) { - this.hash = hash.clone(); - } - - public void setHashInBase64(String hashInBase64) { - hash = Base64.getDecoder().decode(hashInBase64); - } +public record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - public String getHashInBase64() { - return Base64.getEncoder().encodeToString(hash); + /** + * Creates {@link SignableHash} instance, + *

    + * Will use SHA-512 as the default hashing algorithm + * + * @param hashToSign byte array of hash to be signed + * @throws SmartIdRequestSetupException when hashToSign is missing or empty + */ + public SignableHash(byte[] hashToSign) { + this(hashToSign, HashAlgorithm.SHA_512); } - public HashType getHashType() { - return hashType; + /** + * Creates {@link SignableHash} instance + * + * @param hashToBeSigned byte array of hash to be signed + * @param hashAlgorithm hashing algorithm used to create the hash + * @throws SmartIdRequestSetupException when input parameters are missing or empty + */ + public SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) { + validateInputs(hashToBeSigned, hashAlgorithm); + this.hashToBeSigned = hashToBeSigned.clone(); + this.hashAlgorithm = hashAlgorithm; } - public void setHashType(HashType hashType) { - this.hashType = hashType; + private static void validateInputs(byte[] hash, HashAlgorithm hashAlgorithm) { + if (hash == null || hash.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'hash' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } } - public boolean areFieldsFilled() { - return hashType != null && hash != null && hash.length > 0; + /** + * Get the hash as Base64-encoded string + * + * @return String + */ + @Override + public String getDigestInBase64() { + return Base64.getEncoder().encodeToString(hashToBeSigned); } } diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index 3d3b8eb7..177cb869 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java @@ -44,7 +44,7 @@ public class VerificationCodeCalculator { * @return verification code. */ public static String calculate(byte[] documentHash) { - byte[] digest = DigestCalculator.calculateDigest(documentHash, HashType.SHA256); + byte[] digest = DigestCalculator.calculateDigest(documentHash, HashAlgorithm.SHA_256); ByteBuffer byteBuffer = ByteBuffer.wrap(digest); int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 int rightMostBytesIndex = byteBuffer.limit() - shortBytes; diff --git a/src/main/java/ee/sk/smartid/util/SignatureUtil.java b/src/main/java/ee/sk/smartid/util/SignatureUtil.java deleted file mode 100644 index 80e39be6..00000000 --- a/src/main/java/ee/sk/smartid/util/SignatureUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.SignableData; -import ee.sk.smartid.SignableHash; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for signature-related operations. - */ -public final class SignatureUtil { - - private SignatureUtil() { - } - - /** - * Decides which of the two parameters to use for obtaining the Base64-encoded digest to sign. - * - * @param signableHash value to be sent for signing on data that was already hashed - * @param signableData value to be hashed before sending for signing - * @return Base64-encoded digest - */ - public static String getDigestToSignBase64(SignableHash signableHash, SignableData signableData) { - if (signableHash != null && signableHash.areFieldsFilled()) { - return signableHash.getHashInBase64(); - } else if (signableData != null) { - if (signableData.getHashType() == null) { - throw new SmartIdClientException("HashType must be set for signableData."); - } - return signableData.calculateHashInBase64(); - } else { - throw new SmartIdClientException("Either signableHash or signableData must be set."); - } - } -} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 6ee36e54..b41bb68b 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -67,7 +67,6 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; class DeviceLinkAuthenticationSessionRequestBuilderTest { diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index a302a49a..0d25f510 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -254,7 +254,7 @@ void buildDeviceLink(SessionType sessionType) { .withSessionType(sessionType) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) + .withInteractions(BASE64_INTERACTIONS) // // TODO - 07.09.25: fix this, if certificate choice then it should be empty .withLang(LANGUAGE) .withElapsedSeconds(1L) .withRelyingPartyName(RELYING_PARTY_NAME); diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 2d2724cd..16287347 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -37,10 +37,10 @@ import static org.mockito.Mockito.when; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -53,6 +53,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; @@ -61,114 +62,91 @@ import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureSessionRequest; class DeviceLinkSignatureSessionRequestBuilderTest { + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + private SmartIdConnector connector; - private DeviceLinkSignatureSessionRequestBuilder builder; @BeforeEach void setUp() { connector = mock(SmartIdConnector.class); - - builder = new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) - .withSignableData(new SignableData("Test data".getBytes())) - .withInitialCallbackUrl("https://example.com/callback"); } @Test void initSignatureSession_withSemanticsIdentifier() { - var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - builder.withSemanticsIdentifier(semanticsIdentifier); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockSignatureSessionResponse()); - - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - assertNotNull(signature); - assertEquals("test-session-id", signature.sessionID()); - assertEquals("test-session-token", signature.sessionToken()); - assertEquals("test-session-secret", signature.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(semanticsIdentifier)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); + assertNotNull(signatureSessionResponse); + assertEquals("test-session-id", signatureSessionResponse.sessionID()); + assertEquals("test-session-token", signatureSessionResponse.sessionToken()); + assertEquals("test-session-secret", signatureSessionResponse.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signatureSessionResponse.deviceLinkBase()); } @Test void initSignatureSession_withDocumentNumber() { - String documentNumber = "PNOEE-31111111111"; - builder.withDocumentNumber(documentNumber); - + String documentNumber = "PNOEE-31111111111-MOCK-Q"; when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signature); assertEquals("test-session-id", signature.sessionID()); assertEquals("test-session-token", signature.sessionToken()); assertEquals("test-session-secret", signature.sessionSecret()); assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), eq(documentNumber)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { - builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); + assertNotNull(signatureSessionResponse); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.certificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) void initSignatureSession_withNonce_ok(String nonce) { - builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); + assertNotNull(signatureSessionResponse); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.nonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } @Test void initSignatureSession_withRequestProperties() { - builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); + assertNotNull(signatureSessionResponse); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); @@ -176,62 +154,68 @@ void initSignatureSession_withRequestProperties() { SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertNotNull(capturedRequest.requestProperties()); assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(HashType.SHA256); - builder.withSignableData(signableData) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); + assertNotNull(signatureSessionResponse); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @ParameterizedTest - @EnumSource(HashType.class) - void initSignatureSession_withSignableHash(HashType hashType) { - var signableHash = new SignableHash(); - signableHash.setHash("Test hash".getBytes()); - signableHash.setHashType(hashType); - builder.withSignableData(null) - .withSignableHash(signableHash) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignablData(HashAlgorithm hashAlgorithm) { + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); + assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); } @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { - builder.withCapabilities(capabilities) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signature); @@ -240,19 +224,14 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex SignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(expectedCapabilities, capturedRequest.capabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(HashType.SHA512); - builder.withSignableData(signableData) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(mockSignatureSessionResponse()); - - DeviceLinkSessionResponse signature = builder.initSignatureSession(); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signature); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); @@ -268,133 +247,114 @@ class ErrorCases { @ParameterizedTest @NullAndEmptySource void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { - builder.withDocumentNumber(documentNumber).withSemanticsIdentifier(null); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed.", ex.getMessage()); } @Test void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(HashType.SHA256); - builder.withSignableData(signableData) - .withSignatureAlgorithm(null) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); } @Test - void initSignatureSession_whenHashTypeIsNull_throwsException() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(null); - builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_signableDataWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("HashType must be set for signableData.", ex.getMessage()); + @Test + void initSignatureSession_signableHashWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); } @Test - void initSignatureSession_whenSignableHashAndDataAreNull_usesDefaultSignatureAlgorithm() { - builder.withSignableHash(null) - .withSignableData(null) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_whenSignableHashAndDataAreNull_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); } - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_whenInteractionsIsNullOrEmpty(List interactions) { - builder.withInteractions(interactions); + @Test + void initSignatureSession_signableHashBeingSetAfterSignableData_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> toBaseDeviceLinkSessionRequestBuilder() + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableData.", ex.getMessage()); + } - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + @Test + void initSignatureSession_signableDataBeingSetAfterSignableHash_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withSignableHash(new SignableHash("Test data".getBytes())) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableHash.", ex.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSignableData(new SignableData("test".getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in"))) - .withInitialCallbackUrl(url) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .initSignatureSession() - ); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); } + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_whenInteractionsIsNullOrEmpty(List interactions) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + @ParameterizedTest @ArgumentsSource(DuplicateInteractionsProvider.class) void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withSignableData(new SignableData("data".getBytes(StandardCharsets.UTF_8))) - .withInteractions(duplicateInteractions) - .initSignatureSession() - ); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); - assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { - builder.withRelyingPartyUUID(relyingPartyUUID); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { - builder.withRelyingPartyName(relyingPartyName); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } - @Test - void initSignatureSession_invalidNonce() { - builder.withNonce("1234567890123456789012345678901"); - - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); - assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); - } - - @Test - void initSignatureSession_emptyNonce() { - builder.withNonce(""); + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - var ex = assertThrows(SmartIdRequestSetupException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); } - - @Test - void initSignatureSession_whenSignableHashNotFilled() { - var signableHash = new SignableHash(); - builder.withSignableData(null) - .withSignableHash(signableHash) - .withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); - } } @Nested @@ -407,11 +367,10 @@ void validateResponseParameters_missingSessionID(String sessionID) { "test-session-token", "test-session-secret", URI.create("https://example.com/device-link")); - - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); } @@ -422,11 +381,10 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { sessionToken, "test-session-secret", URI.create("https://example.com/device-link")); - - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); } @@ -437,11 +395,10 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { "test-session-token", sessionSecret, URI.create("https://example.com/device-link")); - - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); } @@ -452,15 +409,28 @@ void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String d "test-session-token", "test-session-secret", deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); } } + private DeviceLinkSignatureSessionRequestBuilder toDeviceLinkSignatureSessionRequestBuilder(UnaryOperator builder) { + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + return builder.apply(deviceLinkSessionRequestBuilder); + } + + private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestBuilder() { + return new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withSignableData(new SignableData("Test data".getBytes())); + } + private DeviceLinkSessionResponse mockSignatureSessionResponse() { return new DeviceLinkSessionResponse("test-session-id", "test-session-token", diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index a5052380..aa6ad3f0 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,9 +32,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; import org.apache.commons.codec.binary.Hex; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import ee.sk.smartid.exception.permanent.SmartIdClientException; @@ -42,33 +48,32 @@ public class DigestCalculatorTest { private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); - @Test - public void calculateDigest_sha256() { - byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA256); + @ParameterizedTest + @ArgumentsSource(DigestAlgorithmValueProvider.class) + public void calculateDigest_sha256(HashAlgorithm hashAlgorithm, String expectedHex) { + byte[] sha = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, hashAlgorithm); - assertThat(Hex.encodeHexString(sha512), - is("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069")); + assertThat(Hex.encodeHexString(sha), is(expectedHex)); } @Test - public void calculateDigest_sha384() { - byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA384); - - assertThat(Hex.encodeHexString(sha512), - is("bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a")); + public void calculateDigest_nullHashType() { + var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); } - @Test - public void calculateDigest_sha512() { - byte[] sha512 = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, HashType.SHA512); - - assertThat(Hex.encodeHexString(sha512), - is("861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8")); - } + private static class DigestAlgorithmValueProvider implements ArgumentsProvider { - @Test - public void calculateDigest_nullHashType() { - var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); - assertEquals("Parameter 'hashType' must be set", ex.getMessage()); + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(HashAlgorithm.SHA_256, "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"), + Arguments.of(HashAlgorithm.SHA_384, "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a"), + Arguments.of(HashAlgorithm.SHA_512, "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"), + Arguments.of(HashAlgorithm.SHA3_256, "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af"), + Arguments.of(HashAlgorithm.SHA3_384, "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c"), + Arguments.of(HashAlgorithm.SHA3_512, "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48") + ); + } } } diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index af66b5aa..e421218a 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -198,11 +198,9 @@ void initSignatureSession_withRequestProperties() { @Disabled("Signature algorithm has changed") @ParameterizedTest - @EnumSource(HashType.class) - void initSignatureSession_withSignableHash(HashType hashType) { - var signableHash = new SignableHash(); - signableHash.setHash("Test hash".getBytes()); - signableHash.setHashType(hashType); + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); @@ -214,7 +212,7 @@ void initSignatureSession_withSignableHash(HashType hashType) { verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(hashType.getHashTypeName().toLowerCase() + "WithRSAEncryption", capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(hashAlgorithm.getAlgorithmName().toLowerCase() + "WithRSAEncryption", capturedRequest.signatureProtocolParameters().signatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @@ -239,10 +237,9 @@ void initSignatureSession_withCapabilities(Set capabilities, Set } @ParameterizedTest - @EnumSource(HashType.class) - void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashType hashType) { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(hashType); + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashAlgorithm hashAlgorithm) { + var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); @@ -262,7 +259,6 @@ void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashT @Test void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(HashType.SHA512); builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); @@ -307,7 +303,6 @@ void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { @Test void initSignatureSession_whenSignableDataHashTypeIsNull() { SignableData signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(null); builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); SmartIdClientException exception = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); @@ -317,7 +312,6 @@ void initSignatureSession_whenSignableDataHashTypeIsNull() { @Test void initSignatureSession_whenHashTypeIsNull() { var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(null); builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); @@ -364,15 +358,6 @@ void initSignatureSession_emptyNonce() { var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); } - - @Test - void initSignatureSession_whenSignableHashNotFilled() { - var signableHash = new SignableHash(); - builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Either signableHash or signableData must be set.", ex.getMessage()); - } } @Nested diff --git a/src/test/java/ee/sk/smartid/SignatureUtilTest.java b/src/test/java/ee/sk/smartid/SignatureUtilTest.java deleted file mode 100644 index 07005219..00000000 --- a/src/test/java/ee/sk/smartid/SignatureUtilTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Base64; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.SignatureUtil; - -class SignatureUtilTest { - - @Test - void getDigestToSignBase64_withSignableHash() { - var signableHash = new SignableHash(); - signableHash.setHash("Test hash".getBytes()); - signableHash.setHashType(HashType.SHA256); - - String digestBase64 = SignatureUtil.getDigestToSignBase64(signableHash, null); - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), digestBase64); - } - - @Test - void getDigestToSignBase64_withSignableData() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(HashType.SHA256); - - String digestBase64 = SignatureUtil.getDigestToSignBase64(null, signableData); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), digestBase64); - } - - @Test - void getDigestToSignBase64_throwsExceptionWhenNoHashOrData() { - var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(null, null)); - assertEquals("Either signableHash or signableData must be set.", exception.getMessage()); - } - - @Test - void getDigestToSignBase64_throwsExceptionWhenHashTypeIsNullInSignableData() { - var signableData = new SignableData("Test data".getBytes()); - signableData.setHashType(null); - - var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(null, signableData)); - assertEquals("HashType must be set for signableData.", exception.getMessage()); - } - - @Test - void getDigestToSignBase64_withSignableHashFieldsNotFilled() { - var signableHash = new SignableHash(); - signableHash.setHash(new byte[0]); - signableHash.setHashType(HashType.SHA256); - - var exception = assertThrows(SmartIdClientException.class, () -> SignatureUtil.getDigestToSignBase64(signableHash, null)); - assertEquals("Either signableHash or signableData must be set.", exception.getMessage()); - } - - @Test - void setHashInBase64_shouldDecodeBase64String() { - var signableHash = new SignableHash(); - String base64EncodedHash = Base64.getEncoder().encodeToString("Test hash".getBytes()); - - signableHash.setHashInBase64(base64EncodedHash); - - assertEquals(base64EncodedHash, signableHash.getHashInBase64()); - } -} diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 539bad03..902f478f 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -50,7 +50,6 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -230,13 +229,10 @@ void createDeviceLinkSignature_withDocumentNumberSameDevice() { "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) @@ -255,13 +251,10 @@ void createDeviceLinkSignature_withDocumentNumberQrCode() { "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -279,13 +272,9 @@ void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); - + var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) @@ -304,13 +293,10 @@ void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -354,7 +340,6 @@ void getCertificateByDocumentNumber_withUnknownState_throwsException() { .withCertificateLevel(CertificateLevel.ADVANCED); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); } } @@ -412,10 +397,7 @@ void createNotificationSignature_withDocumentNumber() { "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); - signableHash.setHashType(HashType.SHA512); - + var signableHash = new SignableHash("a".repeat(64).getBytes()); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withDocumentNumber(DOCUMENT_NUMBER) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) @@ -435,10 +417,7 @@ void createNotificationSignature_withSemanticsIdentifier() { "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(64).getBytes())); - signableHash.setHashType(HashType.SHA512); - + var signableHash = new SignableHash("a".repeat(64).getBytes()); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") @@ -592,13 +571,10 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { "requests/sign/device-link/signature/device-link-signature-request-same-device.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) @@ -611,7 +587,7 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { .withSessionType(SessionType.SIGNATURE) .withSessionToken(response.sessionToken()) .withLang("eng") - .withDigest(signableHash.getHashInBase64()) + .withDigest(signableHash.getDigestInBase64()) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .buildDeviceLink(response.sessionSecret()); @@ -624,13 +600,10 @@ void createDynamicContent_withQrCode() { "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -645,7 +618,7 @@ void createDynamicContent_withQrCode() { .withSessionType(SessionType.SIGNATURE) .withSessionToken(response.sessionToken()) .withLang("eng") - .withDigest(signableHash.getHashInBase64()) + .withDigest(signableHash.getDigestInBase64()) .buildDeviceLink(response.sessionSecret()); assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); @@ -657,13 +630,10 @@ void createDynamicContent_withQrCodeImage() { "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - var signableHash = new SignableHash(); - signableHash.setHashInBase64(Base64.toBase64String("a".repeat(32).getBytes())); - signableHash.setHashType(HashType.SHA512); + var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withHashAlgorithm(HashAlgorithm.SHA_512) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -677,7 +647,7 @@ void createDynamicContent_withQrCodeImage() { .withSessionType(SessionType.SIGNATURE) .withSessionToken(response.sessionToken()) .withLang("eng") - .withDigest(signableHash.getHashInBase64()) + .withDigest(signableHash.getDigestInBase64()) .buildDeviceLink(response.sessionSecret()); String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index bce47f41..986a9e61 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -54,7 +54,7 @@ public void calculateCorrectVerificationCode() { private void assertVerificationCode(String verificationCode, String dataString) { byte[] data = dataString.getBytes(StandardCharsets.UTF_8); - byte[] hash = DigestCalculator.calculateDigest(data, HashType.SHA256); + byte[] hash = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); assertEquals(verificationCode, VerificationCodeCalculator.calculate(hash)); } } diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index f945eaa9..9e29dcdc 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -62,7 +62,7 @@ import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.FileTrustedCAStoreBuilder; -import ee.sk.smartid.HashType; +import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.QrCodeGenerator; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; @@ -78,13 +78,13 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.DeviceLinkUtil; @Disabled("Replace relying party UUID and name with your own values in setup") @SmartIdDemoIntegrationTest @@ -341,17 +341,15 @@ void signature_withDocumentNumberAndQRCode() { // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA256); + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); // Build the dynamic link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document")); DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) - .withHashAlgorithm(HashAlgorithm.SHA_256) - .withInteractions(List.of( - DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInteractions(signatureInteractions) .initSignatureSession(); // Process the signature response @@ -376,6 +374,7 @@ void signature_withDocumentNumberAndQRCode() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") + .withInteractions(DeviceLinkUtil.encodeToBase64(signatureInteractions)) .buildDeviceLink(sessionSecret); // Return URI to be used with QR-code generation library on the frontend side @@ -438,8 +437,7 @@ void signature_withSemanticIdentifier() { // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); // Create the signable data - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA512); + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); var semanticsIdentifier = new SemanticsIdentifier( // 3 character identity type @@ -449,12 +447,12 @@ void signature_withSemanticIdentifier() { "40504040001"); // identifier (according to country and identity type reference) // Build the dynamic link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document")); DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInteractions(signatureInteractions) .initSignatureSession(); // Process the signature response @@ -479,6 +477,7 @@ void signature_withSemanticIdentifier() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") + .withInteractions(DeviceLinkUtil.encodeToBase64(signatureInteractions)) .buildDeviceLink(sessionSecret); // Display QR-code to the user @@ -674,8 +673,7 @@ void signature_withSemanticsIdentifier() { // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); // Create the signable data - var signableData = new SignableData("dataToSign".getBytes()); - signableData.setHashType(HashType.SHA512); + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); // Create the Semantics Identifier var semanticsIdentifier = new SemanticsIdentifier( diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 63684591..2d9cf819 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -40,7 +40,7 @@ import org.junit.jupiter.api.Test; import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashType; +import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SignatureAlgorithm; @@ -53,7 +53,6 @@ import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -130,7 +129,7 @@ private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionReq var signatureParameters = new AcspV2SignatureProtocolParameters( RpChallengeGenerator.generate(), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new AuthenticationSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, @@ -174,9 +173,9 @@ class Signature { @Test void initDeviceLinkSignature_withSemanticIdentifier() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); var request = new SignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, @@ -200,9 +199,9 @@ void initDeviceLinkSignature_withSemanticIdentifier() { @Test void initDeviceLinkSignature_withDocumentNumber() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); var request = new SignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, @@ -257,7 +256,7 @@ private static AuthenticationSessionRequest toAuthenticationRequest() { var signatureParameters = new AcspV2SignatureProtocolParameters( RpChallengeGenerator.generate(), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new AuthenticationSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, @@ -312,9 +311,9 @@ void initNotificationCertificateChoice_withDocumentNumber() { private static SignatureSessionRequest toSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashType.SHA512)), + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new SignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, "QUALIFIED", diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index e4aed58a..2594f6b9 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -56,6 +56,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.SignatureProtocol; import ee.sk.smartid.SmartIdRestServiceStubs; @@ -73,7 +74,6 @@ import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.HashAlgorithm; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -1102,7 +1102,7 @@ private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionReq var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( Base64.toBase64String("a".repeat(32).getBytes()), "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getValue())); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new AuthenticationSessionRequest( "00000000-0000-0000-0000-000000000000", From 6976bf83183796dd5906801430bd7b038b213c86 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 15 Sep 2025 14:40:46 +0300 Subject: [PATCH 41/57] Add linked notification based signing (#134) * SLIB-117 - add path for creating linked signature session with request and response objects * SLIB-117 - add builder to create linked notification signature session; update Readme and example tests * SLIB-117 - fix FlowType enum value converting from string * SLIB-117 - add missing license headers * SLIB-117 - reduce duplicated code in SmartIdRestConnector * SLIB-117 - improve usage of SignableHash and SignableData validations; improve code style * SLIB-117 - fix tests and improve code style in SmartIdRestConnectorTest --- CHANGELOG.md | 4 + README.md | 151 ++++++---- ...icationSignatureSessionRequestBuilder.java | 277 ++++++++++++++++++ .../smartid/SignatureResponseValidator.java | 2 +- .../java/ee/sk/smartid/SmartIdClient.java | 11 + .../ee/sk/smartid/rest/SmartIdConnector.java | 10 + .../sk/smartid/rest/SmartIdRestConnector.java | 139 +++------ .../dao/LinkedSignatureSessionRequest.java | 43 +++ .../dao/LinkedSignatureSessionResponse.java | 38 +++ .../ee/sk/smartid/util/DeviceLinkUtil.java | 33 ++- ...ionSignatureSessionRequestBuilderTest.java | 212 ++++++++++++++ .../SignatureResponseValidatorTest.java | 29 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 69 ++++- .../integration/ReadmeIntegrationTest.java | 76 +++++ .../rest/SmartIdRestConnectorTest.java | 277 ++++++++++++------ ...ate-choice-session-request-all-fields.json | 0 ...te-choice-session-request-device-link.json | 0 ...te-choice-session-request-for-qr-code.json | 0 ...-signature-session-request-all-fields.json | 19 ++ ...-session-request-only-required-fields.json | 14 + .../session-status-successful-signature.json | 26 +- ...k-certificate-choice-session-response.json | 0 ...tification-signature-session-response.json | 4 + 23 files changed, 1161 insertions(+), 273 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java create mode 100644 src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java rename src/test/resources/requests/sign/{device-link => linked}/cert-choice/certificate-choice-session-request-all-fields.json (100%) rename src/test/resources/requests/sign/{device-link => linked}/cert-choice/certificate-choice-session-request-device-link.json (100%) rename src/test/resources/requests/sign/{device-link => linked}/cert-choice/certificate-choice-session-request-for-qr-code.json (100%) create mode 100644 src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json create mode 100644 src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json rename src/test/resources/responses/sign/{device-link => linked}/certificate-choice/device-link-certificate-choice-session-response.json (100%) create mode 100644 src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d094f5..069ca05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.13] - 2025-09-08 +- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. +- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. + ## [3.1.12] - 2025-09-08 - Removed HashType and update SignableHash and SignableData to use HashAlgorithm diff --git a/README.md b/README.md index 45401a03..d6486fe3 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,6 @@ This library supports Smart-ID API v3.1. * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) * [Initiating a dynamic-link authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) - * [Device link certificate choice session](#device-link-certificate-choice-session) - * [Examples of initiating a device-link certificate choice session](#examples-of-initiating-a-device-link-certificate-choice-session) - * [Initiating device-link certificate choice](#initiating-an-anonymous-certificate-choice-session) * [Device-link signature session](#device-link-signature-session) * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) @@ -56,8 +53,13 @@ This library supports Smart-ID API v3.1. * [Error handling for session status](#error-handling-for-session-status) * [Certificate by document number](#certificate-by-document-number) * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) + * [Linked signature session flow](#linked-signature-flow) + * [Device link certificate choice session](#device-link-certificate-choice-session) + * [Examples of initiating a device-link certificate choice session](#example-of-initiating-a-device-link-certificate-choice-session) + * [Linked notification-based signature session](#linked-notification-based-signature-session) + * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) * [Notification-based flows](#notification-based-flows) - * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-dynamic-link-flows) + * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-device-link-flows) * [Notification-based authentication session](#notification-based-authentication-session) * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) @@ -323,52 +325,6 @@ Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -### Device-link certificate choice session - -The Smart-ID API v3.1 introduces device-link certificate choice session. This allows more secure way of initiating signing. -Scanning QR-code or clicking on device link will prove that the certificates of the device being used for signing is in the proximity where the signing was initiated. -The certificate choice session must be followed by a linked notification-based signature session. - -#### Request Parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. - -#### Examples of initiating a device-link certificate choice session - -##### Initiating an anonymous certificate choice session -```java -DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) - .initiateCertificateChoice(); - -String sessionId = certificateChoice.sessionID(); -// SessionID is used to query sessions status later - -String sessionToken = certificateChoice.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = certificateChoice.sessionSecret(); -String deviceLinkBase = certificateChoice.deviceLinkBase(); -Instant responseReceivedAt = certificateChoice.receivedAt(); -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - ### Device-link signature session #### Request Parameters @@ -917,6 +873,101 @@ certificateValidator.validateCertificate(certResponse.certificate()); ``` Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). +## Linked signature flow + +In API v3.1 a new flow was introduced to link signature session to a previously completed certificate choice session. +The flow starts off with device link certificate choice session and must be followed by a linked notification-based signature session. + +### Device link certificate choice session + +Anonymous device link certificate choice session can be initiated without knowing the user's document number. When the session is completed successfully, +the Smart-ID API will stay waiting for the RP to start the [linked notification-based signature session](#linked-notification-based-signature-session). + +#### Request Parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. +* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. + +#### Example of initiating a device-link certificate choice session + +```java +DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .initiateCertificateChoice(); + +String sessionId = certificateChoice.sessionID(); +// SessionID is used to query sessions status later + +String sessionToken = certificateChoice.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = certificateChoice.sessionSecret(); +String deviceLinkBase = certificateChoice.deviceLinkBase(); +Instant responseReceivedAt = certificateChoice.receivedAt(); +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Linked notification-based signature session + +Second part of the linked signature flow. Will be used to start the signature session after the device link certificate choice session is completed successfully. + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `linkedSessionID`: Required. Session ID of the previously completed certificate choice session. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response parameters + +* `sessionID`: Required. String that can be used to request the signature session status result. + +#### Example of initiating a linked notification-based signature session + +```java +// Prerequisite: device link certificate choice has been completed successfully. +DeviceLinkSessionResponse certificateChoiceSessionResponse; +CertificateChoiceResponse certificateChoiceResponse; + +// Start the linked notification signature session using the sessionID from the certificate choice session +LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) + .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))) + .initSignatureSession(); + +// SessionID is used to query sessions status later +String sessionId = signatureSessionResponse.sessionID(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + ## Notification-based flows ### Differences between notification-based and device link flows diff --git a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java new file mode 100644 index 00000000..a2953efc --- /dev/null +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -0,0 +1,277 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for initializing a linked notification signature session request. + * Must follow an anonymous device link certificate choice session + */ +public class LinkedNotificationSignatureSessionRequestBuilder { + + private final SmartIdConnector smartIdConnector; + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private DigestInput digestInput; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private String linkedSessionID; + private List interactions; + private CertificateLevel certificateLevel; + private String nonce; + private Boolean shareIpAddress; + private Set capabilities; + + /** + * Initializes the builder with the given Smart ID connector. + * + * @param smartIdConnector the Smart-ID connector + */ + public LinkedNotificationSignatureSessionRequestBuilder(SmartIdConnector smartIdConnector) { + this.smartIdConnector = smartIdConnector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the signable data. + * + * @param signableData the data to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableHash + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (digestInput != null && digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableHash"); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the signable hash. + * + * @param signableHash the hash to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableData + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (digestInput != null && digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableData"); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sets the signature algorithm. + * . + * + * @param signatureAlgorithm The signature algorithm + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the linked session ID. + * + * @param linkedSessionID the session ID from the device link certificate choice session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withLinkedSessionID(String linkedSessionID) { + this.linkedSessionID = linkedSessionID; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the interactions. + * + * @param interactions list of interactions to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the mobile device's IP address with the relying party. + * + * @param shareIpAddress true to share the IP address, false otherwise + * @return this + */ + public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareIpAddress) { + this.shareIpAddress = shareIpAddress; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = Set.of(capabilities); + return this; + } + + /** + * Initializes the linked notification signature session. + * + * @return The linked signature session response + * @throws SmartIdRequestSetupException when any required parameter is missing or invalid + * @throws UnprocessableSmartIdResponseException when server response is missing required fields + */ + public LinkedSignatureSessionResponse initSignatureSession() { + validateRequestParameters(); + LinkedSignatureSessionRequest request = createSessionRequest(); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = smartIdConnector.initLinkedNotificationSignature(request, documentNumber); + validateResponse(linkedSignatureSessionResponse); + return linkedSignatureSessionResponse; + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (StringUtil.isEmpty(documentNumber)) { + throw new SmartIdRequestSetupException("Value for 'documentNumber' cannot be empty"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); + } + if (StringUtil.isEmpty(linkedSessionID)) { + throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must be 1-30 characters long"); + } + if (interactions == null || interactions.isEmpty()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private LinkedSignatureSessionRequest createSessionRequest() { + var rawDigestParams = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + return new LinkedSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + rawDigestParams, + linkedSessionID, + nonce, + DeviceLinkUtil.encodeToBase64(interactions), + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, + capabilities); + } + + private void validateResponse(LinkedSignatureSessionResponse linkedSignatureSessionResponse) { + if (StringUtil.isEmpty(linkedSignatureSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Linked notification-base signature session response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index 572738dc..ea85f0ea 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -117,7 +117,7 @@ public SignatureResponse validate(SessionStatus sessionStatus, rsaSsaPssParams.setTrailerField(TrailerField.OXBC); signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); - signatureResponse.setFlowType(FlowType.valueOf(sessionSignature.getFlowType())); + signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 32b4f274..490f9bd0 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -78,6 +78,17 @@ public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertific .withRelyingPartyName(relyingPartyName); } + /** + * Creates a new builder for creating a linked notification signature session request. + * + * @return a builder for creating a new linked notification signature session request + */ + public LinkedNotificationSignatureSessionRequestBuilder createLinkedNotificationSignature() { + return new LinkedNotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + /** * Creates a new builder for creating a notification certificate choice session request. * diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index b92ea262..aebc9d23 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -34,6 +34,8 @@ import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; @@ -71,6 +73,14 @@ public interface SmartIdConnector extends Serializable { */ DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoiceSessionRequest request); + /** + * Initiates a linked notification based signature session. + * + * @param request LinkedSignatureSessionRequest containing necessary parameters + * @return LinkedSignatureSessionResponse containing sessionID + */ + LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); + /** * Initiates a notification based certificate choice request. * diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 4b31366f..3ddaac08 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -44,15 +44,17 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SessionStatusRequest; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @@ -73,12 +75,15 @@ public class SmartIdRestConnector implements SmartIdConnector { @Serial - private static final long serialVersionUID = 2024_10_18; + private static final long serialVersionUID = 2025_09_10L; private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; + private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; @@ -137,7 +142,7 @@ public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSess .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); } @Override @@ -147,7 +152,7 @@ public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSess .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); } @Override @@ -156,7 +161,7 @@ public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(Authentic URI uri = UriBuilder.fromUri(endpointUrl) .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) .build(); - return postDeviceLinkAuthenticationRequest(uri, authenticationRequest); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); } @Override @@ -166,7 +171,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postNotificationAuthenticationRequest(uri, authenticationRequest); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); } @Override @@ -176,7 +181,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postNotificationAuthenticationRequest(uri, authenticationRequest); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); } @Override @@ -186,8 +191,18 @@ public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoi .fromUri(endpointUrl) .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } - return postDeviceLinkCertificateChoiceRequest(uri, request); + @Override + public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber) { + logger.debug("Starting linked notification-based signature session"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, LinkedSignatureSessionResponse.class); } @Override @@ -197,7 +212,7 @@ public NotificationCertificateChoiceSessionResponse initNotificationCertificateC .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postNotificationCertificateChoiceRequest(uri, request); + return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); } public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { @@ -206,7 +221,7 @@ public CertificateResponse getCertificateByDocumentNumber(String documentNumber, .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postCertificateByDocumentNumberRequest(uri, request); + return postRequest(uri, request, CertificateResponse.class); } @Override @@ -216,7 +231,7 @@ public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postDeviceLinkSignatureRequest(uri, request); + return postRequest(uri, request, DeviceLinkSessionResponse.class); } @Override @@ -226,25 +241,27 @@ public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postDeviceLinkSignatureRequest(uri, request); + return postRequest(uri, request, DeviceLinkSessionResponse.class); } + @Override public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) .build(); - return postNotificationSignatureRequest(uri, request); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); } + @Override public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) .path(documentNumber) .build(); - return postNotificationSignatureRequest(uri, request); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); } @Override @@ -298,90 +315,6 @@ protected String getJdkMajorVersion() { } } - private DeviceLinkSessionResponse postDeviceLinkAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { - try { - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } - - private NotificationAuthenticationSessionResponse postNotificationAuthenticationRequest(URI uri, AuthenticationSessionRequest request) { - try { - return postRequest(uri, request, NotificationAuthenticationSessionResponse.class); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException e) { - logger.warn("No permission to issue the request", e); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", e); - } - } - - private DeviceLinkSessionResponse postDeviceLinkCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { - try { - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } catch (NotFoundException ex) { - logger.warn("User account not found for URI {}", uri, ex); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } - } - - private NotificationCertificateChoiceSessionResponse postNotificationCertificateChoiceRequest(URI uri, CertificateChoiceSessionRequest request) { - try { - return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); - } catch (NotFoundException ex) { - logger.warn("User account not found for URI {}", uri, ex); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } - } - - private CertificateResponse postCertificateByDocumentNumberRequest(URI uri, CertificateByDocumentNumberRequest request) { - try { - return postRequest(uri, request, CertificateResponse.class); - } catch (NotFoundException ex) { - logger.warn("User account not found for URI {}", uri, ex); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } - } - - private DeviceLinkSessionResponse postDeviceLinkSignatureRequest(URI uri, SignatureSessionRequest request) { - try { - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } catch (NotFoundException ex) { - logger.warn("User account not found for URI " + uri, ex); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } - } - - private NotificationSignatureSessionResponse postNotificationSignatureRequest(URI uri, SignatureSessionRequest request) { - try { - return postRequest(uri, request, NotificationSignatureSessionResponse.class); - } catch (NotFoundException ex) { - logger.warn("User account not found for URI {}", uri, ex); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } - } - private T postRequest(URI uri, V request, Class responseType) { try { Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); @@ -392,6 +325,12 @@ private T postRequest(URI uri, V request, Class responseType) { } catch (BadRequestException ex) { logger.warn("Request is invalid for URI {}", uri, ex); throw new SmartIdClientException("Server refused the request", ex); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); } catch (ClientErrorException ex) { if (ex.getResponse().getStatus() == 471) { logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java new file mode 100644 index 00000000..e482f026 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java @@ -0,0 +1,43 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public record LinkedSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + String linkedSessionID, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java new file mode 100644 index 00000000..eede94a4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java @@ -0,0 +1,38 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Response for linked notification based signature session initiation. + * + * @param sessionID The session ID + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record LinkedSignatureSessionResponse(String sessionID) { +} diff --git a/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java b/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java index 878cea9b..2f0a93da 100644 --- a/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java +++ b/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,25 +26,38 @@ * #L% */ -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; - import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Utility class for interactions related actions + */ public class DeviceLinkUtil { private static final ObjectMapper mapper = new ObjectMapper(); - public static String encodeToBase64(List interactions) { + private DeviceLinkUtil() { + } + + /** + * Encodes list of interactions to Base64 string + * + * @param interactions list of interactions + * @return base64 encoded string + * @throws SmartIdClientException if unable to encode interactions + */ + public static String encodeToBase64(List interactions) { try { String json = mapper.writeValueAsString(interactions); return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { - throw new SmartIdClientException("Unable to encode interactions to base64", e); + } catch (JsonProcessingException ex) { + throw new SmartIdClientException("Unable to encode interactions to Base64", ex); } } } diff --git a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java new file mode 100644 index 00000000..e36aecf9 --- /dev/null +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -0,0 +1,212 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; + +class LinkedNotificationSignatureSessionRequestBuilderTest { + + private static final String DOCUMENT_NUMBER = "PNOEE-12345678901-MOCK-Q"; + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_ok() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @ParameterizedTest + @EnumSource(CertificateLevel.class) + void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel certificateLevel) { + LinkedNotificationSignatureSessionRequestBuilder builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @Nested + class ValidateRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_documentNumberIsEmpty_throwException(String documentNumber) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataOrSignableHashNotProvided_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with SignableData or with SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAlreadyUsedForSettingDigest_throwException() { + var builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512)))); + assertEquals("Value for 'digestInput' has been already set with SignableData", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException() { + var builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withLinkedSessionID(linkedSessionID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'linkedSessionID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"1234567890123456789012345678901", ""}) + void initSignatureSession_nonceWithIncorrectLengthProvided_throwException(String nonce) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'nonce' must be 1-30 characters long", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_interactionsInEmpty_throwException(List interactions) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_interactionsContainDuplicates_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> + b.withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"), + DeviceLinkInteraction.displayTextAndPIN("Sign again?")))); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + } + + @Test + void initSignatureSession_sessionIDMissingFromResponse_throwException() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse(null)); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Linked notification-base signature session response field 'sessionID' is missing or empty", ex.getMessage()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toLinkedNotificationSignatureSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseLinkedNotificationSignatureSessionRequestBuilder()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificationSignatureSessionRequestBuilder() { + return new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))); + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index 016f6d6b..e1ae0809 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -90,6 +90,16 @@ void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certifica assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); } + @ParameterizedTest + @EnumSource(FlowType.class) + void validate_flowTypesAreSupported(FlowType flowType) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss", flowType); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + @Test void validate_nqSigning_ok() { SessionStatus sessionStatus = toNqignatureSessionStatus(); @@ -491,7 +501,15 @@ void validate_invalidTrailerField() { } } - private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, String signatureAlgorithm) { + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm) { + return toQualifiedSignatureSessionStatus(signatureProtocol, signatureAlgorithm, FlowType.QR); + } + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm, + FlowType flowType) { var sessionResult = new SessionResult(); sessionResult.setEndResult("OK"); sessionResult.setDocumentNumber("PNOEE-12345678901"); @@ -501,7 +519,7 @@ private static SessionStatus toQualifiedSignatureSessionStatus(String signatureP sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params); + var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params, flowType); var sessionStatus = new SessionStatus(); sessionStatus.setState("COMPLETE"); @@ -524,7 +542,7 @@ private static SessionStatus toNqignatureSessionStatus() { sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params); + var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params, FlowType.QR); var sessionStatus = new SessionStatus(); sessionStatus.setState("COMPLETE"); @@ -539,14 +557,15 @@ private static SessionStatus toNqignatureSessionStatus() { private static SessionSignature toSessionSignature(String signatureValue, String signatureAlgorithm, - SessionSignatureAlgorithmParameters params) { + SessionSignatureAlgorithmParameters params, + FlowType flowType) { var sessionSignature = new SessionSignature(); sessionSignature.setValue(signatureValue); sessionSignature.setSignatureAlgorithm(signatureAlgorithm); sessionSignature.setSignatureAlgorithmParameters(params); sessionSignature.setServerRandom("serverRandomValue"); sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); - sessionSignature.setFlowType("QR"); + sessionSignature.setFlowType(flowType.getDescription()); return sessionSignature; } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 902f478f..47c546f5 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -50,6 +50,7 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -82,8 +83,8 @@ class DeviceLinkCertificateChoiceSession { @Test void createSameDeviceCertificateChoiceSession() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withCertificateLevel(CertificateLevel.QUALIFIED) @@ -100,8 +101,8 @@ void createSameDeviceCertificateChoiceSession() { @Test void createSameDeviceCertificateChoiceSessionWithAllFields() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withCertificateLevel(CertificateLevel.QUALIFIED) @@ -120,8 +121,8 @@ void createSameDeviceCertificateChoiceSessionWithAllFields() { @Test void createQrCodeCertificateChoiceSession() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withCertificateLevel(CertificateLevel.ADVANCED) @@ -434,6 +435,50 @@ void createNotificationSignature_withSemanticsIdentifier() { } } + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationBasedSignatureSession { + + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + + @Test + void createLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))) + .initSignatureSession(); + + assertNotNull(response); + } + + @Test + void createLinkedNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withNonce("cmFuZG9tTm9uY2U=") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))) + .withShareMdClientIpAddress(true) + .initSignatureSession(); + + assertNotNull(response); + } + } + @Nested @WireMockTest(httpPort = 18089) class SessionsStatus { @@ -665,8 +710,8 @@ class DynamicContentForCertificateChoice { @Test void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -689,8 +734,8 @@ void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { @Test void createDynamicContent_createQrCodeImage() { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withNonce(Base64.toBase64String("randomNonce".getBytes())) @@ -719,8 +764,8 @@ void createDynamicContent_createQrCodeImage() { @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() .withCertificateLevel(CertificateLevel.QUALIFIED) diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 9e29dcdc..531446d2 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -78,6 +78,7 @@ import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; @@ -739,6 +740,81 @@ void queryCertificate() { } } + @Nested + class LinkedNotificationBasedSignatureSession { + + @Test + void signing_withQrCode() { + DeviceLinkSessionResponse certificateChoiceSessionResponse = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .initCertificateChoice(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Use sessionID to start polling for session status + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = certificateChoiceSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); + URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in dynamic link and in authCode + Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); + + // Build the device link URI + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(sessionToken) + .withLang("est") + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for certificate choice session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus certificateSessionStatus = poller.fetchFinalSessionStatus(certificateChoiceSessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", certificateSessionStatus.getState()); + + // Validate the certificate choice response + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().build()); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + + // Start the linked notification signature session using the sessionID from the certificate choice session + LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionId) + .withSignableData(signableData) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))) + .initSignatureSession(); + + // Use sessionId to poll for signature session status updates + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionResponse.sessionID()); + assertEquals("COMPLETED", signatureSessionStatus.getState()); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + + assertNotNull(signatureResponse.getSignatureValue()); + } + } + private static KeyStore getKeystore() { try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 2594f6b9..8212a726 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -56,6 +56,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.CertificateLevel; import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.SignatureProtocol; @@ -74,11 +75,14 @@ import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionSignature; @@ -137,14 +141,7 @@ void getSessionStatus_forSuccessfulAuthenticationRequest() { assertEquals("QR", sessionSignature.getFlowType()); assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); - SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - assertEquals("SHA3-512", signatureAlgorithmParameters.getHashAlgorithm()); - var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); - assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); - SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); - assertEquals("SHA3-512", parameters.getHashAlgorithm()); - assertEquals(64, signatureAlgorithmParameters.getSaltLength()); - assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); + assertSignatureAlgorithmParameters(sessionSignature, "SHA3-512"); assertNotNull(sessionStatus.getCert()); assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); @@ -168,9 +165,13 @@ void getSessionStatus_forSuccessfulSignatureRequest() { assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); - assertNotNull(sessionStatus.getSignature()); - assertThat(sessionStatus.getSignature().getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); - assertEquals("sha256WithRSAEncryption", sessionStatus.getSignature().getSignatureAlgorithm()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertThat(sessionSignature.getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + assertSignatureAlgorithmParameters(sessionSignature, "SHA-512"); assertNotNull(sessionStatus.getCert()); assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); @@ -308,6 +309,16 @@ private static void assertSuccessfulResponse(SessionStatus sessionStatus) { assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); } + private static void assertSignatureAlgorithmParameters(SessionSignature sessionSignature, String expectedHashAlgorithm) { + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + assertEquals(expectedHashAlgorithm, signatureAlgorithmParameters.getHashAlgorithm()); + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); + SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); + assertEquals(expectedHashAlgorithm, parameters.getHashAlgorithm()); + assertEquals(64, signatureAlgorithmParameters.getSaltLength()); + assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); + } } @Nested @@ -442,6 +453,7 @@ void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException( class SemanticsIdentifierNotificationAuthentication { private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); private SmartIdRestConnector connector; @@ -454,7 +466,7 @@ void setUp() { @Test void initNotificationAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); assertNotNull(response); } @@ -463,7 +475,7 @@ void initNotificationAuthentication() { void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); }); } @@ -472,7 +484,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); }); } } @@ -483,6 +495,7 @@ void initNotificationAuthentication_requestIsUnauthorized_throwException() { class DocumentNumberNotificationAuthentication { private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; private SmartIdRestConnector connector; @@ -495,7 +508,8 @@ void setUp() { @Test void initNotificationAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); assertNotNull(response); } @@ -504,7 +518,7 @@ void initNotificationAuthentication() { void initNotificationAuthentication_userAccountNotFound_throwException() { assertThrows(UserAccountNotFoundException.class, () -> { SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); }); } @@ -513,7 +527,7 @@ void initNotificationAuthentication_userAccountNotFound_throwException() { void initNotificationAuthentication_requestIsUnauthorized_throwException() { assertThrows(RelyingPartyAccountConfigurationException.class, () -> { SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); }); } } @@ -533,7 +547,7 @@ public void setUp() { @Test void initDeviceLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json"); + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); @@ -583,8 +597,7 @@ void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationExcep CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - + var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); } @@ -612,8 +625,7 @@ void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @@ -639,11 +651,129 @@ private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest } } + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationSignature { + + private static final String LINKED_SIGNATURE_PATH = "/signature/notification/linked/PNOEE-31111111111-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111-MOCK-Q"; + private static final String NONCE = "cmFuZG9tTm9uY2U="; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionRequest request = toLinkedSignatureSessionRequest(null, null, null); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = connector.initLinkedNotificationSignature(request, DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_withAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse linkedSignatureSessionResponse = + connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_documentNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_ApiClientIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + private LinkedSignatureSessionRequest toFullLinkedSignatureSessionRequest() { + return toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, NONCE, new RequestProperties(true)); + } + + private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(CertificateLevel certificateLevel, + String nonce, + RequestProperties requestProperties) { + var rawDigestSignatureProtocolParameters = new RawDigestSignatureProtocolParameters( + "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); + return new LinkedSignatureSessionRequest("00000000-0000-0000-0000-000000000000", + "DEMO", + certificateLevel != null ? certificateLevel.name() : null, + "RAW_DIGEST_SIGNATURE", + rawDigestSignatureProtocolParameters, + "10000000-0000-000-000-000000000000", + nonce, + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + requestProperties, + null); + } + } + @Nested @WireMockTest(httpPort = 18089) class SemanticsIdentifierNotificationCertificateChoiceTests { private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/certificatechoice/notification/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); private SmartIdRestConnector connector; @@ -668,18 +798,20 @@ void initCertificateChoice_withSemanticsIdentifier_successful() { @Test void initCertificateChoice_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/certificate-choice-session-request.json"); - connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); - }); + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/certificate-choice-session-request.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), SEMANTICS_IDENTIFIER)); } @Test void initCertificateChoice_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/certificate-choice-session-request.json"); - connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), new SemanticsIdentifier("PNOEE-31111111111")); - }); + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/certificate-choice-session-request.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), SEMANTICS_IDENTIFIER)); } private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { @@ -700,6 +832,7 @@ private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest class CertificateByDocumentNumberTests { private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; private SmartIdRestConnector connector; @@ -712,7 +845,7 @@ void setUp() { void getCertificateByDocumentNumber_successful() { SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); - CertificateResponse response = connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest()); assertNotNull(response); assertEquals("OK", response.state()); @@ -726,7 +859,7 @@ void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", null); - CertificateResponse response = connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", certificateByDocumentNumberRequest); + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); assertNotNull(response); assertEquals("OK", response.state()); @@ -738,17 +871,15 @@ void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { @Test void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(UserAccountNotFoundException.class, () -> { - connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); - }); + assertThrows(UserAccountNotFoundException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); } @Test void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - connector.getCertificateByDocumentNumber("PNOEE-30303039914-MOCK-Q", toCertificateByDocumentNumberRequest()); - }); + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); } } @@ -757,6 +888,7 @@ void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { class DeviceLinkSignatureTests { private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); private SmartIdRestConnector connector; @@ -769,11 +901,9 @@ public void setUp() { @Test void initDeviceLinkSignature_withSemanticsIdentifier_successful() { stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, semanticsIdentifier); + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); assertNotNull(response); assertEquals("test-session-id", response.sessionID()); @@ -801,41 +931,33 @@ void initDeviceLinkSignature_withDocumentNumber_successful() { @Test void initDeviceLinkSignature_userAccountNotFound() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initDeviceLinkSignature_relyingPartyNoPermission() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initDeviceLinkSignature_invalidRequest() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); } @@ -845,9 +967,8 @@ void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundExceptio stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @Test @@ -855,20 +976,16 @@ void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initDeviceLinkSignature_throwsSmartIdClientException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - Exception exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); } @@ -877,9 +994,8 @@ void initDeviceLinkSignature_throwsServerMaintenanceException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); SignatureSessionRequest request = createSignatureSessionRequest(); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, semanticsIdentifier)); + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } } @@ -997,6 +1113,7 @@ void initNotificationSignature_throwsServerMaintenanceException() { class DocumentNumberNotificationSignature { private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; private SmartIdRestConnector connector; @@ -1009,11 +1126,9 @@ void setUp() { @Test void initNotificationSignature() { SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, documentNumber); + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); assertNotNull(response); assertNotNull(response.getSessionID()); @@ -1028,10 +1143,7 @@ void initNotificationSignature_userAccountNotFound_throwException() { SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @Disabled("Request body has changed") @@ -1041,10 +1153,7 @@ void initNotificationSignature_requestIsUnauthorized_throwException() { SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @Test @@ -1053,10 +1162,7 @@ void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcept SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @Test @@ -1065,10 +1171,7 @@ void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @Test @@ -1077,10 +1180,7 @@ void initNotificationSignature_throwsSmartIdClientException() { SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - var ex = assertThrows(SmartIdClientException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); } @@ -1091,10 +1191,7 @@ void initNotificationSignature_throwsServerMaintenanceException() { SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - assertThrows(ServerMaintenanceException.class, () -> { - String documentNumber = "PNOEE-48010010101-MOCK-Q"; - connector.initNotificationSignature(request, documentNumber); - }); + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } } diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json similarity index 100% rename from src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-all-fields.json rename to src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json similarity index 100% rename from src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-device-link.json rename to src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json diff --git a/src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json similarity index 100% rename from src/test/resources/requests/sign/device-link/cert-choice/certificate-choice-session-request-for-qr-code.json rename to src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json new file mode 100644 index 00000000..9aaf3ba7 --- /dev/null +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json @@ -0,0 +1,19 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "nonce": "cmFuZG9tTm9uY2U=", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + "requestProperties": { + "shareMdClientIpAddress": true + } +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json new file mode 100644 index 00000000..e4cf80d4 --- /dev/null +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json @@ -0,0 +1,14 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d" +} \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json index e009ee1b..f5372743 100644 --- a/src/test/resources/responses/session-status-successful-signature.json +++ b/src/test/resources/responses/session-status-successful-signature.json @@ -3,19 +3,35 @@ "result": { "endResult": "OK", "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, "signatureProtocol": "RAW_DIGEST_SIGNATURE", "signature": { "value": "fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgLLScB4+0qEhji9HKNRNHpXsip6LmoDiWD7pBlBPL0YOsFczSEpRpCe3NLxWWCWzd7i6tFcYXFwhpXEaUtoUhpstGOtjYGHkvXzMcQmiyXC4qWrw3RrSqEnB2ONmuZE60brwyRne8xYgBMmHcvu0s9jcTDWM+ppNfjm4WED+u5sOTGbSyO7Eg9kOhfDenYg++1Cg6zlpWwd9OMpojmK2pOsZC0JmcOIyQ+Cf2mBobx0qt6cPot9/bx1X5uTualJfxMrRZSE3twuXq0f3f0A+Yv3kHhx/AdzQaAuydtIdlz60naWIS84PUnAeOKiYLRbRRawLc4MGZHqn4DeFHI4zvzMLhz13O8pirFWb7qWJ+RvsgyAMTHmAwzPmtpwYT90z22Bc915qTufaJ48/m8DXGARQdbOP+/+5a4Q7PwnrdAm7SwbnNcAlvzVQO+o1onhnPKGz79EYVIgNj+9Hijqdggw41lBEjl82Lr7LNuVhz2wVaBYD4yELzmoDEOW69wWMQ6WHwK/SF1Xe44ENi6JSZE1f19AQT0+xOt0FWKloQ9Tn/kvtw+/LhLzugOtf61t9HBLCt73iSpJ6SqD4lMHxozJ5SEJNm05DBhaCf3IlZzw0HYFRMZNUXx/7y2QhOWpRMFZIhjHjFedi1IxPj3BmKTL1Vgq5koCDxF1Wbdl+UONK9UthYpKpU13Wi04YubYLb3VKw9wb9f9YlweXoeUHxOTy3l6f+Z6lP3EYAp7NbyJlPCW7yhTeS4kg4uzftqr+2cW4ORdQvs2Va7qrkdu5sd8d72jKWuQluviR5gCTLvQtttc/Tex/ix8iuQ4ffHTap+gnrcEgIA3Th8Z0m93kwpE+YLjHAxMQmzgkR/iPoDpTutpqjoLbrhLgUQpSJ5pYyRQgc6iM/BN6+xpe2GFBoODXzBj81OK1qDN89A26ldyLDan0tkSKIuVJWIapDxQick", - "signatureAlgorithm": "sha256WithRSAEncryption", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, "cert": { "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" }, "interactionTypeUsed": "verificationCodeChoice", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + "deviceIpAddress": "203.0.113.34", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } diff --git a/src/test/resources/responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json similarity index 100% rename from src/test/resources/responses/sign/device-link/certificate-choice/device-link-certificate-choice-session-response.json rename to src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json diff --git a/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json new file mode 100644 index 00000000..29b7df71 --- /dev/null +++ b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json @@ -0,0 +1,4 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file From f4548837399600b965dd5438992c091c98350419 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 17 Sep 2025 15:13:23 +0300 Subject: [PATCH 42/57] Add missing validation and typo fixes (#136) * SLIB-117 - add additional validation; rename 0XBC to only BC; fix typo in exception message * SLIB-117 - improve testing and validations of capabilities for signature session requests --- .../AuthenticationResponseMapperImpl.java | 2 +- ...iceLinkSignatureSessionRequestBuilder.java | 3 +- ...icationSignatureSessionRequestBuilder.java | 7 ++- .../smartid/SignatureResponseValidator.java | 8 +-- src/main/java/ee/sk/smartid/TrailerField.java | 2 +- .../dao/LinkedSignatureSessionRequest.java | 14 +++++ src/main/java/ee/sk/smartid/util/SetUtil.java | 55 +++++++++++++++++++ .../java/ee/sk/smartid/util/StringUtil.java | 31 +++++++++-- .../AuthenticationResponseValidatorTest.java | 2 +- .../smartid/CapabilitiesArgumentProvider.java | 48 ++++++++++++++++ ...inkSignatureSessionRequestBuilderTest.java | 28 ++++++---- ...ionSignatureSessionRequestBuilderTest.java | 46 ++++++++++++++++ .../SignatureResponseValidatorTest.java | 2 +- .../SignatureValueValidatorImplTest.java | 2 +- 14 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/util/SetUtil.java create mode 100644 src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index e4473f29..4edab95c 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -244,7 +244,7 @@ private static void validateSignatureAlgorithmParameters(SessionSignature sessio if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); } - if (!TrailerField.OXBC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { + if (!TrailerField.BC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 25b19b5d..d65a983a 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -42,6 +42,7 @@ import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** @@ -148,7 +149,7 @@ public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { * @return this builder */ public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } diff --git a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java index a2953efc..045d5105 100644 --- a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -40,6 +40,7 @@ import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** @@ -146,7 +147,6 @@ public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(Signabl /** * Sets the signature algorithm. - * . * * @param signatureAlgorithm The signature algorithm * @return this builder @@ -207,7 +207,7 @@ public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddre * @return this builder */ public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } @@ -239,6 +239,9 @@ private void validateRequestParameters() { if (digestInput == null) { throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } if (StringUtil.isEmpty(linkedSessionID)) { throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index ea85f0ea..44ee19e1 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -114,7 +114,7 @@ public SignatureResponse validate(SessionStatus sessionStatus, rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - rsaSsaPssParams.setTrailerField(TrailerField.OXBC); + rsaSsaPssParams.setTrailerField(TrailerField.BC); signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); @@ -309,9 +309,9 @@ private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorit } if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { - logger.error("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); } if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { @@ -329,7 +329,7 @@ private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorit throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); } - if (!TrailerField.OXBC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { + if (!TrailerField.BC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); } diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index b7302790..9966b2ec 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -33,7 +33,7 @@ */ public enum TrailerField { - OXBC("0xbc", 1); + BC("0xbc", 1); private final String value; private final int pssSpecValue; diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java index e482f026..ebad4ab8 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java @@ -30,6 +30,20 @@ import com.fasterxml.jackson.annotation.JsonInclude; +/** + * Linked signature session request + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Certificate level. Possible values: QSCD, QUALIFIED, ADVANCED, + * @param signatureProtocol Required. Signature protocol. Only RAW_DIGEST_SIGNATURE is supported for signing. + * @param signatureProtocolParameters Required. RAW_DIGEST_SIGNATURE signature protocol parameters + * @param linkedSessionID Required. ID of the anonymous certificate choice session to be linked with this signature session. + * @param nonce Random value to cancel out idempotence of the request. + * @param interactions Required. Device link interactions should be used. + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + */ public record LinkedSignatureSessionRequest(String relyingPartyUUID, String relyingPartyName, @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, diff --git a/src/main/java/ee/sk/smartid/util/SetUtil.java b/src/main/java/ee/sk/smartid/util/SetUtil.java new file mode 100644 index 00000000..9ea2a928 --- /dev/null +++ b/src/main/java/ee/sk/smartid/util/SetUtil.java @@ -0,0 +1,55 @@ +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for Set operations. + */ +public final class SetUtil { + + private SetUtil() { + } + + /** + * Converts an array to a Set, filtering out null or empty values. + * + * @param array array to be converted + * @return a set of non-null, non-empty trimmed strings + */ + public static Set toSet(String[] array) { + return Arrays.stream(array) + .filter(Objects::nonNull) + .map(String::trim) + .filter(StringUtil::isNotEmpty) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index 1093da3a..6274c4e8 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,18 +26,41 @@ * #L% */ -public class StringUtil { +/** + * Utility class to handle string operations + */ +public final class StringUtil { + + private StringUtil() { + } + /** + * Checks that given CharSequence is not null and not empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is not null and not empty, false otherwise + */ public static boolean isNotEmpty(final CharSequence cs) { return cs != null && !cs.isEmpty(); } + /** + * Checks that given CharSequence is null or empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is null or empty, false otherwise + */ public static boolean isEmpty(final CharSequence cs) { return cs == null || cs.isEmpty(); } + /** + * Checks that given string is not null and not empty + * + * @param input the value to check + * @return String if the input is not null and not empty, empty string otherwise + */ public static String orEmpty(String input) { return input == null ? "" : input; } - } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 3db5f281..9a066db9 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -184,7 +184,7 @@ private static SessionStatus toSessionsStatus(String certificateValue, String ce var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.OXBC.getValue()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); diff --git a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java new file mode 100644 index 00000000..98b362d3 --- /dev/null +++ b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java @@ -0,0 +1,48 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class CapabilitiesArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")), + Arguments.of(new String[]{"capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), + Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 16287347..3b74c25b 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -209,6 +209,23 @@ void initSignatureSession_withSignablData(HashAlgorithm hashAlgorithm) { assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockSignatureSessionResponse()); + + DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); + assertEquals("test-session-id", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + SignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { @@ -449,17 +466,6 @@ public Stream provideArguments(ExtensionContext context) { } } - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[]{"QUALIFIED", "ADVANCED"}, Set.of("QUALIFIED", "ADVANCED")), - Arguments.of(new String[]{"QUALIFIED"}, Set.of("QUALIFIED")), - Arguments.of(new String[]{}, Set.of()) - ); - } - } - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java index e36aecf9..65029467 100644 --- a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -31,18 +31,22 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; +import java.util.Set; import java.util.function.UnaryOperator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; @@ -88,6 +92,40 @@ void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel cer assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + @Nested class ValidateRequestParameters { @@ -149,6 +187,14 @@ void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); } + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + @ParameterizedTest @NullAndEmptySource void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index e1ae0809..d93356cc 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -459,7 +459,7 @@ void validate_mismatchedHashAlgorithms() { sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); } @Test diff --git a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java index 2e9ec703..4ab8d7ac 100644 --- a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java @@ -103,7 +103,7 @@ private static RsaSsaPssParameters toRsaSsaPssParameters() { rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); - rsaSsaPssParameters.setTrailerField(TrailerField.OXBC); + rsaSsaPssParameters.setTrailerField(TrailerField.BC); return rsaSsaPssParameters; } From d8184d9f7854b094e8134cbd96cc921a767e23a4 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 18 Sep 2025 22:21:02 +0300 Subject: [PATCH 43/57] Update notification based authentication (#135) * SLIB-117 - add path for creating linked signature session with request and response objects * SLIB-117 - reduce duplicated code in SmartIdRestConnector * SLIB-109 - add notification-based authentication request object and update builder * SLIB-109 - change RpChallengeGenerator to return RpChallenge object instead of string * SLIB-109 - update request validation exception messages * SLIB-109 - change interactions for device-link and notification-based flows * SLIB-109 - remove redundant code; update documentation; update validations * SLIB-109 - improve documentation; improve interactions text to be more descriptive in examples * SLIB-109 - remove AuthenticationHash; update ErrorResultHandler javadoc * SLIB-109 - update old usage of dynamic link to device link * SLIB-109 - improve code style --- CHANGELOG.md | 5 + README.md | 199 +++--- .../ee/sk/smartid/AuthenticationHash.java | 86 --- .../AuthenticationResponseValidator.java | 14 +- ...nkAuthenticationSessionRequestBuilder.java | 29 +- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 2 +- ...iceLinkSignatureSessionRequestBuilder.java | 38 +- .../ee/sk/smartid/ErrorResultHandler.java | 24 +- ...icationSignatureSessionRequestBuilder.java | 10 +- ...onAuthenticationSessionRequestBuilder.java | 166 ++--- ...icationSignatureSessionRequestBuilder.java | 20 +- .../java/ee/sk/smartid/RpChallenge.java} | 39 +- .../ee/sk/smartid/RpChallengeGenerator.java | 26 +- .../smartid/VerificationCodeCalculator.java | 60 +- ...ionFlow.java => VerificationCodeType.java} | 34 +- .../InteractionType.java} | 21 +- .../smartid/common/InteractionValidator.java | 55 ++ .../sk/smartid/common/InteractionsMapper.java | 60 ++ .../sk/smartid/common/SmartIdInteraction.java | 54 ++ .../interactions/DeviceLinkInteraction.java | 78 +++ .../DeviceLinkInteractionType.java} | 30 +- .../interactions/NotificationInteraction.java | 88 +++ .../NotificationInteractionType.java | 57 ++ ...serRefusedVerificationChoiceException.java | 33 - .../ee/sk/smartid/rest/SmartIdConnector.java | 13 +- .../sk/smartid/rest/SmartIdRestConnector.java | 15 +- .../AcspV2SignatureProtocolParameters.java | 7 + .../dao/AuthenticationSessionRequest.java | 44 -- ...eviceLinkAuthenticationSessionRequest.java | 57 ++ .../rest/dao/DeviceLinkInteraction.java | 66 -- .../rest/dao/DeviceLinkSessionResponse.java | 14 +- .../ee/sk/smartid/rest/dao/Interaction.java | 81 +-- ...ificationAuthenticationSessionRequest.java | 56 ++ ...ficationAuthenticationSessionResponse.java | 28 +- .../rest/dao/NotificationInteraction.java | 63 -- .../smartid/rest/dao/RequestProperties.java | 5 + .../dao/SignatureAlgorithmParameters.java | 10 +- .../sk/smartid/rest/dao/VerificationCode.java | 1 + ...viceLinkUtil.java => InteractionUtil.java} | 8 +- .../AuthenticationResponseValidatorTest.java | 14 +- ...thenticationSessionRequestBuilderTest.java | 513 ++++++---------- ...inkSignatureSessionRequestBuilderTest.java | 49 +- ...licateDeviceLinkInteractionsProvider.java} | 32 +- ...tificationInteractionArgumentProvider.java | 49 ++ .../ee/sk/smartid/ErrorResultHandlerTest.java | 2 +- .../InvalidRpChallengeArgumentProvider.java | 50 ++ ...ionSignatureSessionRequestBuilderTest.java | 14 +- ...thenticationSessionRequestBuilderTest.java | 565 +++++++----------- ...ionSignatureSessionRequestBuilderTest.java | 4 +- .../sk/smartid/RpChallengeGeneratorTest.java | 25 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 97 ++- ...erRefusedInteractionArgumentsProvider.java | 2 - .../VerificationCodeCalculatorTest.java | 47 +- .../common/InteractionValidatorTest.java | 73 +++ .../common/InteractionsMapperTest.java | 88 +++ .../DeviceLinkInteractionTest.java | 100 ++++ .../NotificationInteractionTest.java | 125 ++++ .../integration/ReadmeIntegrationTest.java | 128 ++-- .../SmartIdRestIntegrationTest.java | 48 +- .../rest/SmartIdRestConnectorTest.java | 519 +++++++++++++--- ...ation-session-request-invalid-request.json | 7 + ...thentication-session-request-qr-code.json} | 2 +- ...ession-request-same-device-all-fields.json | 18 + ...uest-same-device-only-required-fields.json | 15 + ...entication-session-request-all-fields.json | 18 + ...uthentication-session-request-invalid.json | 4 + ...session-request-only-required-fields.json} | 13 +- ...-link-authentication-session-response.json | 0 .../notification-session-response.json | 4 + .../notification-session-response.json | 9 - 70 files changed, 2570 insertions(+), 1760 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/AuthenticationHash.java rename src/{test/java/ee/sk/smartid/InteractionUtil.java => main/java/ee/sk/smartid/RpChallenge.java} (64%) rename src/main/java/ee/sk/smartid/{rest/dao/DeviceLinkInteractionFlow.java => VerificationCodeType.java} (66%) rename src/main/java/ee/sk/smartid/{rest/dao/InteractionFlow.java => common/InteractionType.java} (67%) create mode 100644 src/main/java/ee/sk/smartid/common/InteractionValidator.java create mode 100644 src/main/java/ee/sk/smartid/common/InteractionsMapper.java create mode 100644 src/main/java/ee/sk/smartid/common/SmartIdInteraction.java create mode 100644 src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java rename src/main/java/ee/sk/smartid/{rest/dao/NotificationInteractionFlow.java => common/devicelink/interactions/DeviceLinkInteractionType.java} (65%) create mode 100644 src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java create mode 100644 src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java delete mode 100644 src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java delete mode 100644 src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java rename src/main/java/ee/sk/smartid/util/{DeviceLinkUtil.java => InteractionUtil.java} (91%) rename src/{main/java/ee/sk/smartid/util/NotificationUtil.java => test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java} (59%) create mode 100644 src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java create mode 100644 src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java create mode 100644 src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java create mode 100644 src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java create mode 100644 src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java create mode 100644 src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json rename src/test/resources/requests/{device-link-authentication-session-request.json => auth/device-link/device-link-authentication-session-request-qr-code.json} (100%) create mode 100644 src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json create mode 100644 src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json create mode 100644 src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json create mode 100644 src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json rename src/test/resources/requests/{notification-authentication-session-request.json => auth/notification/notification-authentication-session-request-only-required-fields.json} (50%) rename src/test/resources/responses/{ => auth/device-link}/device-link-authentication-session-response.json (100%) create mode 100644 src/test/resources/responses/auth/notification/notification-session-response.json delete mode 100644 src/test/resources/responses/notification-session-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 069ca05e..734072ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.14] - 2025-09-17 +- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 +- Removed verificationCodeChoice interactions and related handling +- Removed AuthenticationHash. + ## [3.1.13] - 2025-09-08 - Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. - Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. diff --git a/README.md b/README.md index d6486fe3..bc13a3d3 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This library supports Smart-ID API v3.1. * [Linked notification-based signature session](#linked-notification-based-signature-session) * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) * [Notification-based flows](#notification-based-flows) - * [Differences between notification-based and dynamic link flows](#differences-between-notification-based-and-device-link-flows) + * [Differences between notification-based and device link flows](#differences-between-notification-based-and-device-link-flows) * [Notification-based authentication session](#notification-based-authentication-session) * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) @@ -71,7 +71,7 @@ This library supports Smart-ID API v3.1. * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) - * [Examples of allowed notification-based interactions order](#examples-of-allowed-notification-based-interactions-order) + * [Examples of allowed notification-based interactions order](#examples-of-notification-based-interactions-order) * [Exception handling](#exception-handling) * [Network connection configuration of the client](#network-connection-configuration-of-the-client) * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) @@ -114,7 +114,7 @@ Changes introduced with new library versions are described in [CHANGELOG.md](CHA # How to use API v3.1 Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. -This version introduces new dynamic link and notification-based flows for authentication, certificate choice and signing. +This version introduces new device link and notification-based flows for authentication, certificate choice and signing. NB! v2 API classes are removed. @@ -207,7 +207,7 @@ DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient // to use anonymous authentication, do not set semantics identifier or document number .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )); // Initiate authentication session @@ -246,17 +246,17 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code "30303039914"); // identifier (according to country and identity type reference) -// For security reasons a new random challenge must be created for each new authentication request -String rpChallenge = RpChallengeGenerator.generate(); -// Store generated randomChallenge only backend side. Do not expose it to the client side. +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )); // Initiate authentication session @@ -288,17 +288,17 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ```java String documentNumber = "PNOLT-40504040001-MOCK-Q"; -// For security reasons a new hash value must be created for each new authentication request -String rpChallenge = RpChallengeGenerator.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating OK authentication sessions status response DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )); // Initiate authentication session @@ -380,7 +380,7 @@ DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) .initSignatureSession(); @@ -413,7 +413,7 @@ DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSign .withSignableData(signableData) .withDocumentNumber(documentNumber) .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. .initSignatureSession(); // Process the signature response @@ -436,7 +436,7 @@ Handle exceptions appropriately. The Java client provides specific exceptions fo ```java try { - DynamicLinkSessionResponse response = builder.init*Session(); + DeviceLinkSessionResponse response = builder.init*Session(); } catch (UserAccountNotFoundException e) { System.out.println("User account not found."); } catch (RelyingPartyAccountConfigurationException e) { @@ -460,12 +460,12 @@ More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3. Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. ```java -DynamicLinkSessionResponse authenticationSessionResponse = client +DeviceLinkSessionResponse authenticationSessionResponse = client .createDeviceLinkAuthentication() - .withRandomChallenge(randomChallenge) + .withRpChallenge(rpChallenge) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" .withInteractions(Collections.singletonList( - DynamicLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )) // setting property to request the IP-address of the user's device .withShareMdClientIpAddress(true) @@ -473,34 +473,26 @@ DynamicLinkSessionResponse authenticationSessionResponse = client ``` ### Examples of device link interactions -Todo: needs to be updated An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. -For dynamic link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. +For device link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons and a second screen to enter the PIN-code. -Below are examples of allowedInteractionsOrder elements specifically for dynamic link flows: +Below are examples of interaction elements specifically for device link flows: -Example 1: `confirmationMessage` with Fallback to `displayTextAndPIN` +Example 1: `confirmationMessage` with fallback to `displayTextAndPIN` Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. ```java builder.withInteractions(List.of( DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), - DeviceLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") + DeviceLinkInteraction.displayTextAndPin("Up to 60 characters of text here..") )) ``` -Example 2: `displayTextAndPIN` Only -Description: Use `displayTextAndPIN` interaction only. -```java -builder.withInteractions(List.of( - DeviceLinkInteraction.displayTextAndPIN("Up to 60 characters of text here..") -)); -``` - -Example 3: `confirmationMessage` Only (No Fallback) -Description: Insist on `confirmationMessage`; if not available, then fail. +Example 2: `confirmationMessage` Only (No Fallback) +Description: Insist on `confirmationMessage`; +NB! If interactions is not supported the process will fail if fallback is not provided. ```java builder.withInteractions(List.of( DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") @@ -523,22 +515,26 @@ Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, * `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. * `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. -* `version`: Version of the dynamic link. Only allowed value is `"1.0"`. -* `deviceLinkType`: Type of the dynamic link. Possible values are `QR`, `Web2App`, `App2App`. -* `sessionType`: Type of the sessions the dynamic link is for. Possible values are `auth`, `sign`, `cert`. +* `version`: Version of the device link. Only allowed value is `"1.0"`. +* `deviceLinkType`: Type of the device link. Possible values are `QR`, `Web2App`, `App2App`. +* `sessionType`: Type of the sessions the device link is for. Possible values are `auth`, `sign`, `cert`. * `sessionToken`: Token from the session response. * `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` -* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a dynamic link +* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a device link * `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. * `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. -* `initialCallbackUrl`: Optional. Initial callback URL to be used for the dynamic link. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. +* `interactions`: Base64-encoded JSON string of an array of interaction objects, used to calculate the authCode. +* `initialCallbackUrl`: Optional. Initial callback URL used for the same device(Web2App or App2App) device link flows. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. ```java +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; + DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. // Calculate elapsed seconds since session response long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() +URI deviceLink = smartIdClient.createDynamicContent() .withDeviceLinkBase(sessionResponse.deviceLinkBase()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.AUTHENTICATION) @@ -546,6 +542,7 @@ URI deviceLink = new DeviceLinkBuilder() .withElapsedSeconds(elapsedSeconds) .withLang("eng") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session .buildDeviceLink(sessionResponse.sessionSecret()); ``` @@ -553,6 +550,7 @@ URI deviceLink = new DeviceLinkBuilder() ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. // Build final device link URI with authCode URI deviceLink = new DeviceLinkBuilder() .withSchemeName("smart-id-demo") // override default scheme name to use demo environment @@ -562,6 +560,7 @@ URI deviceLink = new DeviceLinkBuilder() .withSessionToken(sessionResponse.sessionToken()) .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session .withInitialCallbackUrl("https://your-app/callback") .buildDeviceLink(sessionResponse.sessionSecret()); ``` @@ -578,6 +577,7 @@ Generated QR code will have error correction level low. ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode @@ -589,6 +589,7 @@ URI deviceLink = new DeviceLinkBuilder() .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session .buildDeviceLink(sessionResponse.sessionSecret()); // Generate QR code image from device link URI @@ -606,6 +607,7 @@ The width and height of 1159px produce a QR code with a module size of 19px. ```java DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); // Build final device link URI with authCode @@ -617,12 +619,13 @@ URI deviceLink = new DeviceLinkBuilder() .withLang("est") // override language .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session .buildDeviceLink(sessionResponse.sessionSecret()); // Create QR-code with height and width of 570px and quiet area of 2 modules. BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); -String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); // Return Data URI to frontend and display the QR-code +String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); ``` ## Session status request handling for v3.1 @@ -823,7 +826,7 @@ The session status response may return various error codes indicating the outcom * `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. * `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction was canceled. - * `displayTextAndPIN` - User pressed Cancel on PIN screen (either during displayTextAndPIN or verificationCodeChoice flow). + * `displayTextAndPIN` - User pressed Cancel on PIN screen during displayTextAndPIN flow. * `confirmationMessage` - User cancelled on confirmationMessage screen. * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. * `PROTOCOL_FAILURE`: An error occurred in the signing protocol. @@ -960,7 +963,7 @@ LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLi .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. .initSignatureSession(); // SessionID is used to query sessions status later @@ -991,22 +994,21 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- * `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. * `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `randomChallenge`: Required. Random value with size in range of 32-64 bytes. Must be base64 encoded. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * `rpChallenge`: Required. Random value with size in range of 32-64 bytes. Must be Base64 encoded. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values is 'rsassa-pss' + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. An array of interaction objects defining the interactions in order of preference. * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `vcType`: Required. Type of verification code to be used. Currently, the only allowed value is `numeric4`. #### Response parameters * `sessionID`: Required. String used to request the operation result. -* `verificationCode`: Required. Object describing the Verification Code to be displayed. - * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. - * `value`: Required. Value of the VC code. #### Examples of initiating a notification-based authentication session @@ -1015,26 +1017,26 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ```java String documentNumber = "PNOLT-40504040001-MOCK-Q"; -// For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RandomChallenge.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )) .initAuthenticationSession(); -String sessionId = authenticationSessionResponse.sessionID(); // SessionID is used to query sessions status later - -String verificationCode = authenticationSessionResponse.getVc().getValue(); -// Display the verification code to the user for confirmation +String sessionId = authenticationSessionResponse.sessionID(); ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -1047,26 +1049,26 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( "40504040001" ); -// For security reasons a new hash value must be created for each new authentication request -String randomChallenge = RandomChallenge.generate(); -// Store generated randomChallenge only on backend side. Do not expose it to the client side. +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RandomChallenge.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() .withSemanticsIdentifier(semanticsIdentifier) - .withRandomChallenge(randomChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") - )) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into "))) // Display text should be concise and specific. .initAuthenticationSession(); +// SessionID can be used to query sessions status later String sessionId = authenticationSessionResponse.sessionID(); -// SessionID is used to query sessions status later -String verificationCode = authenticationSessionResponse.getVc().getValue(); -// Display the verification code to the user for confirmation ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -1158,7 +1160,7 @@ NotificationSignatureSessionResponse signatureSessionResponse = client.createNot .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) .withAllowedInteractionsOrder(List.of( - NotificationInteraction.verificationCodeChoice("Please sign the document"))) + NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. .initSignatureSession(); // Process the querying sessions status response @@ -1186,7 +1188,7 @@ NotificationSignatureSessionResponse signatureResponse = client.createNotificati .withSignableData(signableData) .withDocumentNumber(documentNumber) .withAllowedInteractionsOrder(List.of( - NotificationInteraction.verificationCodeChoice("Please sign the document"))) + NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. .initSignatureSession(); // Process the signature response @@ -1231,18 +1233,19 @@ try { Authentication is used as an example, nonce can also be used with certificate choice and signature sessions requests by using method `withNonce("randomValue")`. ```java -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(Collections.singletonList( + NotificationInteraction.confirmationMessage("Please sign the ") // Display text should be concise and specific. )) // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned // set nonce to override idempotent behaviour .withNonce("randomValue") - .initAuthenticationSession(); + .initSignatureSession(); ``` #### Using request properties to request the IP address of the user's device @@ -1256,44 +1259,38 @@ Authentication is used for an example, shareMdClientIpAddress can also be used w NotificationAuthenticationSessionResponse authenticationSessionResponse = client .createNotificationAuthentication() .withDocumentNumber(documentNumber) - .withRandomChallenge(randomChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. )) // setting property to request the IP-address of the user's device .withShareMdClientIpAddress(true) .initAuthenticationSession(); ``` -### Examples of allowed notification-based interactions order +### Examples of notification-based interactions order An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. Different interactions can support different amounts of data to display information to the user. -Below are examples of `allowedInteractionsOrder`. +Below are examples of `interactions`. -Example 1: `confirmationMessageAndVerificationCodeChoice` with Fallback to `verificationCodeChoice` -Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; if not available, then fall back to `verificationCodeChoice`. +Example 1: `confirmationMessageAndVerificationCodeChoice` with fallback to `confirmationMessage` and with fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; The second choice is `confirmationMessage`; The third choice is `displayTextAndPIN`. ```java -builder.withAllowedInteractionsOrder(List.of( +builder.withInteractions(List.of( NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), - NotificationInteraction.verificationCodeChoice("Up to 60 characters of text here...") -)); -``` - -Example 1: `verificationCodeChoice` only -Description: Use `verificationCodeChoice` interaction exclusively. -```java -builder.withAllowedInteractionsOrder(List.of( - NotificationInteraction.verificationCodeChoice("Up to 60 characters of text here...") + NotificationInteraction.confirmationMessage("Up to 200 characters of text here..."), + NotificationInteraction.displayTextAndPin("Up to 60 characters of text here...") )); ``` Example 2: `confirmationMessageAndVerificationCodeChoice` only -Description: Insist on `confirmationMessageAndVerificationCodeChoice`; if not available, then fail. +Description: Use `confirmationMessageAndVerificationCodeChoice` interaction exclusively. +NB! Process will fail when interaction is not supported and there is no fallback ```java -builder.withAllowedInteractionsOrder(List.of( +builder.withInteractions(List.of( NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") )); ``` diff --git a/src/main/java/ee/sk/smartid/AuthenticationHash.java b/src/main/java/ee/sk/smartid/AuthenticationHash.java deleted file mode 100644 index 10dbd070..00000000 --- a/src/main/java/ee/sk/smartid/AuthenticationHash.java +++ /dev/null @@ -1,86 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.SecureRandom; - -/** - * Class containing the hash and its hash type used for authentication - */ -public class AuthenticationHash { - - private byte[] hash; - private HashAlgorithm hashAlgorithm; - - /** - * creates {@link AuthenticationHash} instance - * containing a randomly generated hash - * of the chosen hash type - * - * @param hashAlgorithm hash type of the randomly generated hash - * @return authentication hash - */ - public static AuthenticationHash generateRandomHash(HashAlgorithm hashAlgorithm) { - AuthenticationHash authenticationHash = new AuthenticationHash(); - byte[] generatedDigest = DigestCalculator.calculateDigest(getRandomBytes(), hashAlgorithm); - authenticationHash.setHash(generatedDigest); - authenticationHash.setHashAlgorithm(hashAlgorithm); - return authenticationHash; - } - - /** - * creates {@link AuthenticationHash} instance - * containing a randomly generated SHA-512 hash - * - * @return authentication hash - */ - public static AuthenticationHash generateRandomHash() { - return generateRandomHash(HashAlgorithm.SHA_512); - } - - private static byte[] getRandomBytes() { - byte[] randBytes = new byte[64]; - new SecureRandom().nextBytes(randBytes); - return randBytes; - } - - public byte[] getHash() { - return hash; - } - - public void setHash(byte[] hash) { - this.hash = hash; - } - - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - public void setHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } -} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java index 1d5fc7ff..cefc35c5 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -40,7 +40,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.util.StringUtil; @@ -98,7 +98,7 @@ public static AuthenticationResponseValidator defaultSetupWithCertificateValidat * @return the authentication identity */ public AuthenticationIdentity validate(SessionStatus sessionStatus, - AuthenticationSessionRequest authenticationSessionRequest, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { return validate(sessionStatus, authenticationSessionRequest, schemaName, null); } @@ -113,7 +113,7 @@ public AuthenticationIdentity validate(SessionStatus sessionStatus, * @return the authentication identity */ public AuthenticationIdentity validate(SessionStatus sessionStatus, - AuthenticationSessionRequest authenticationSessionRequest, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { validateInputs(sessionStatus, authenticationSessionRequest, schemaName); @@ -160,7 +160,7 @@ private void validateCertificateLevel(AuthenticationResponse authenticationRespo } private void validateSignature(AuthenticationResponse authenticationResponse, - AuthenticationSessionRequest authenticationSessionRequest, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); @@ -171,7 +171,7 @@ private void validateSignature(AuthenticationResponse authenticationResponse, } private byte[] constructPayload(AuthenticationResponse authenticationResponse, - AuthenticationSessionRequest authenticationSessionRequest, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName, String brokeredRpName) { String[] payload = { @@ -192,7 +192,7 @@ private byte[] constructPayload(AuthenticationResponse authenticationResponse, .getBytes(StandardCharsets.UTF_8); } - private static void validateInputs(SessionStatus sessionStatus, AuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + private static void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { if (sessionStatus == null) { throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); } @@ -204,7 +204,7 @@ private static void validateInputs(SessionStatus sessionStatus, AuthenticationSe } } - private static byte[] calculateInteractionsDigest(AuthenticationSessionRequest authenticationSessionRequest) { + private static byte[] calculateInteractionsDigest(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { byte[] interactions = authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8); return DigestCalculator.calculateDigest(interactions, HashAlgorithm.SHA_256); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index dbb76764..5e7f084e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -30,19 +30,20 @@ import java.util.List; import java.util.Set; +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** @@ -67,7 +68,7 @@ public class DeviceLinkAuthenticationSessionRequestBuilder { private String documentNumber; private String initialCallbackUrl; - private AuthenticationSessionRequest authenticationSessionRequest; + private DeviceLinkAuthenticationSessionRequest authenticationSessionRequest; /** * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector @@ -181,7 +182,7 @@ public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress( * @return this builder */ public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } @@ -242,7 +243,7 @@ public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(Stri */ public DeviceLinkSessionResponse initAuthenticationSession() { validateRequestParameters(); - AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + DeviceLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(deviceLinkAuthenticationSessionResponse); this.authenticationSessionRequest = authenticationRequest; @@ -255,14 +256,14 @@ public DeviceLinkSessionResponse initAuthenticationSession() { * @return the authentication session request * @throws SmartIdClientException when session is not yet initialized and method is called */ - public AuthenticationSessionRequest getAuthenticationSessionRequest() { + public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { if (authenticationSessionRequest == null) { throw new SmartIdClientException("Authentication session request has not been initialized yet"); } return authenticationSessionRequest; } - private DeviceLinkSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { + private DeviceLinkSessionResponse initAuthenticationSession(DeviceLinkAuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null && documentNumber != null) { throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); } @@ -312,11 +313,10 @@ private void validateInteractions() { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } validateNoDuplicateInteractions(); - interactions.forEach(DeviceLinkInteraction::validate); } private void validateNoDuplicateInteractions() { - if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); } } @@ -327,18 +327,18 @@ private void validateInitialCallbackUrl() { } } - private AuthenticationSessionRequest createAuthenticationRequest() { + private DeviceLinkAuthenticationSessionRequest createAuthenticationRequest() { var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, signatureAlgorithm.getAlgorithmName(), new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); - return new AuthenticationSessionRequest( + return new DeviceLinkAuthenticationSessionRequest( relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, SignatureProtocol.ACSP_V2, signatureProtocolParameters, - DeviceLinkUtil.encodeToBase64(interactions), + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, capabilities, initialCallbackUrl @@ -360,7 +360,6 @@ private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuth if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); - } } } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 2555c804..4e51fc4e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -340,7 +340,7 @@ private void validateAuthCodeParams(String unprotectedLink) { if (sessionType != SessionType.CERTIFICATE_CHOICE && StringUtil.isEmpty(digest)) { throw new SmartIdClientException("digest must be set for AUTH or SIGN flows"); } - // TODO - 07.09.25: add interactions validation when only for certificate choice case should not be provided, otherwise required + // TODO - 07.09.25: add interactions validation when only for certificate choice case should not be provided, otherwise required, fix in SLIB-110 if (StringUtil.isEmpty(unprotectedLink)) { throw new SmartIdClientException("unprotected device-link must not be empty"); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index d65a983a..9f6e7e94 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -29,19 +29,19 @@ import java.util.List; import java.util.Set; +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; -import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; @@ -67,6 +67,8 @@ public class DeviceLinkSignatureSessionRequestBuilder { private String initialCallbackUrl; private DigestInput digestInput; + private SignatureSessionRequest signatureSessionRequest; + /** * Constructs a new Smart-ID signature request builder with the given connector. * @@ -252,16 +254,32 @@ public DeviceLinkSessionResponse initSignatureSession() { SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); validateResponseParameters(deviceLinkSignatureSessionResponse); + this.signatureSessionRequest = signatureSessionRequest; return deviceLinkSignatureSessionResponse; } + /** + * Gets the SignatureSessionRequest that was used to initiate the signature session. + *

    + * This method can only be called after {@link #initSignatureSession()} has been invoked. + * + * @return the signature request that was used to initiate the session + * @throws SmartIdClientException if called before initSignatureSession() + */ + public SignatureSessionRequest getSignatureSessionRequest() { + if (signatureSessionRequest == null) { + throw new SmartIdClientException("Signature session has not been initiated yet"); + } + return signatureSessionRequest; + } + private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { if (!StringUtil.isEmpty(documentNumber)) { return connector.initDeviceLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { return connector.initDeviceLinkSignature(request, semanticsIdentifier); } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed."); + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"); } } @@ -276,7 +294,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { signatureProtocolParameters, nonce != null ? nonce : null, capabilities, - DeviceLinkUtil.encodeToBase64(interactions), + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, initialCallbackUrl); } @@ -306,8 +324,9 @@ private void validateInteractions() { if (interactions == null || interactions.isEmpty()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } - validateNoDuplicateInteractions(); - interactions.forEach(DeviceLinkInteraction::validate); + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } } private void validateInitialCallbackUrl() { @@ -331,9 +350,4 @@ private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSign } } - private void validateNoDuplicateInteractions() { - if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index 4868677f..155cb8fd 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,11 +26,13 @@ * #L% */ +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.UserAccountException; +import ee.sk.smartid.exception.UserActionException; import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; import ee.sk.smartid.exception.permanent.ProtocolFailureException; -import ee.sk.smartid.exception.permanent.SmartIdServerException; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; @@ -39,7 +41,6 @@ import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; import ee.sk.smartid.rest.dao.SessionResult; import ee.sk.smartid.util.StringUtil; @@ -49,9 +50,21 @@ */ public class ErrorResultHandler { + /** + * Handles the session result and throws an appropriate exception + * + * @param sessionResult the session result to handle + * @throws SmartIdClientException when input parameter sessionResult is null + * @throws UserActionException sub-exceptions based on end result + * @throws UserAccountException sub-exceptions based on end result + * @throws ProtocolFailureException when there was a error in the process (e.g shcema name incorrect) + * @throws ExpectedLinkedSessionException when different session type was started than expected + * @throws SmartIdServerException when technical error occurred on server side + * @throws UnprocessableSmartIdResponseException when unexpected end result was received + */ public static void handle(SessionResult sessionResult) { if (sessionResult == null) { - throw new SmartIdClientException("Session end result is not provided"); + throw new SmartIdClientException("Parameter 'sessionResult' is not provided"); } switch (sessionResult.getEndResult()) { case "USER_REFUSED" -> throw new UserRefusedException(); @@ -74,7 +87,6 @@ private static void handleUserRefusedInteraction(SessionResult sessionResult) { } switch (sessionResult.getDetails().getInteraction()) { case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); - case "verificationCodeChoice" -> throw new UserRefusedVerificationChoiceException(); case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); diff --git a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java index 045d5105..67785957 100644 --- a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -29,18 +29,18 @@ import java.util.List; import java.util.Set; +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; -import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.DeviceLinkUtil; import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.StringUtil; /** @@ -251,7 +251,7 @@ private void validateRequestParameters() { if (interactions == null || interactions.isEmpty()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } - if (interactions.stream().map(Interaction::getType).distinct().count() != interactions.size()) { + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); } } @@ -267,7 +267,7 @@ private LinkedSignatureSessionRequest createSessionRequest() { rawDigestParams, linkedSessionID, nonce, - DeviceLinkUtil.encodeToBase64(interactions), + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, capabilities); } diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index 1ba08fef..ea3d2721 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -26,37 +26,30 @@ * #L% */ -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** - * Class for building a notification authentication session request + * Class for building a notification-based authentication session request */ public class NotificationAuthenticationSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(NotificationAuthenticationSessionRequestBuilder.class); - private final SmartIdConnector connector; private String relyingPartyUUID; @@ -64,7 +57,7 @@ public class NotificationAuthenticationSessionRequestBuilder { private AuthenticationCertificateLevel certificateLevel; private String rpChallenge; private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private String nonce; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; private List interactions; private Boolean shareMdClientIpAddress; private Set capabilities; @@ -114,15 +107,17 @@ public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(Auth } /** - * Sets the random challenge + * Sets the RP challenge + *

    + * The provided rpChallenge must be a Base64 encoded string *

    - * The provided random challenge must be a Base64 encoded string + * Use {@link ee.sk.smartid.RpChallengeGenerator#generate()} to generate a valid RP challenge * - * @param randomChallenge the signature protocol parameters + * @param rpChallenge RP challenge in Base64 encoded format * @return this builder */ - public NotificationAuthenticationSessionRequestBuilder withRandomChallenge(String randomChallenge) { - this.rpChallenge = randomChallenge; + public NotificationAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; return this; } @@ -138,24 +133,24 @@ public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(Si } /** - * Sets the nonce + * Sets the hash algorithm * - * @param nonce the nonce + * @param hashAlgorithm the hash algorithm * @return this builder */ - public NotificationAuthenticationSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; + public NotificationAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; return this; } /** - * Sets the allowed interactions order + * Sets the interactions * - * @param allowedInteractionsOrder the allowed interactions order + * @param interactions the notification interactions * @return this builder */ - public NotificationAuthenticationSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.interactions = allowedInteractionsOrder; + public NotificationAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; return this; } @@ -177,7 +172,7 @@ public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddres * @return this builder */ public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } @@ -220,137 +215,86 @@ public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String */ public NotificationAuthenticationSessionResponse initAuthenticationSession() { validateRequestParameters(); - AuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(notificationAuthenticationSessionResponse); return notificationAuthenticationSessionResponse; } - private NotificationAuthenticationSessionResponse initAuthenticationSession(AuthenticationSessionRequest authenticationRequest) { + private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } else if (semanticsIdentifier != null) { return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); } else if (documentNumber != null) { return connector.initNotificationAuthentication(authenticationRequest, documentNumber); } else { - throw new SmartIdClientException("Either documentNumber or semanticsIdentifier must be set."); + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); } } private void validateRequestParameters() { if (StringUtil.isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } validateSignatureParameters(); - validateNonce(); - validateAllowedInteractionOrder(); + validateInteractions(); } private void validateSignatureParameters() { if (StringUtil.isEmpty(rpChallenge)) { - logger.error("Parameter randomChallenge must be set"); - throw new SmartIdClientException("Parameter randomChallenge must be set"); - } - byte[] challenge = getDecodedRandomChallenge(); - if (challenge.length < 32 || challenge.length > 64) { - logger.error("Size of parameter randomChallenge must be between 32 and 64 bytes"); - throw new SmartIdClientException("Size of parameter randomChallenge must be between 32 and 64 bytes"); - } - if (signatureAlgorithm == null) { - logger.error("Parameter signatureAlgorithm must be set"); - throw new SmartIdClientException("Parameter signatureAlgorithm must be set"); + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); } - } - - private byte[] getDecodedRandomChallenge() { - Base64.Decoder decoder = Base64.getDecoder(); try { - return decoder.decode(rpChallenge); + Base64.getDecoder().decode(rpChallenge); } catch (IllegalArgumentException e) { - logger.error("Parameter randomChallenge is not a valid Base64 encoded string"); - throw new SmartIdClientException("Parameter randomChallenge is not a valid Base64 encoded string"); + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); } - } - - private void validateNonce() { - if (nonce == null) { - return; + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); } - if (nonce.isEmpty()) { - logger.error("Parameter nonce value has to be at least 1 character long"); - throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); } - if (nonce.length() > 30) { - logger.error("Nonce cannot be longer that 30 chars"); - throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); } } - private void validateAllowedInteractionOrder() { + private void validateInteractions() { if (interactions == null || interactions.isEmpty()) { - logger.error("Parameter allowedInteractionsOrder must be set"); - throw new SmartIdClientException("Parameter allowedInteractionsOrder must be set"); + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); } - interactions.forEach(Interaction::validate); } - private AuthenticationSessionRequest createAuthenticationRequest() { + private NotificationAuthenticationSessionRequest createAuthenticationRequest() { var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters("SHA-512")); + new SignatureAlgorithmParameters(hashAlgorithm.getAlgorithmName())); - return new AuthenticationSessionRequest( + return new NotificationAuthenticationSessionRequest( relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2, + SignatureProtocol.ACSP_V2.name(), signatureProtocolParameters, - encodeInteractionsToBase64(interactions), + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, capabilities, - nonce + VerificationCodeType.NUMERIC4.getValue() ); } private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { - if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); - } - - VerificationCode verificationCode = notificationAuthenticationSessionResponse.getVc(); - if (verificationCode == null) { - logger.error("VC object is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC object is missing from the response"); - } - - String vcType = verificationCode.getType(); - if (StringUtil.isEmpty(vcType)) { - logger.error("VC type is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC type is missing from the response"); - } - - if (!VerificationCode.ALPHA_NUMERIC_4.equals(vcType)) { - logger.error("Unsupported VC type: {}", vcType); - throw new UnprocessableSmartIdResponseException("Unsupported VC type: " + vcType); - } - - if (StringUtil.isEmpty(verificationCode.getValue())) { - logger.error("VC value is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC value is missing from the response"); - } - } - - private String encodeInteractionsToBase64(List interactions) { - try { - var mapper = new ObjectMapper(); - return Base64.getEncoder().encodeToString(mapper.writeValueAsString(interactions).getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { - throw new SmartIdClientException("Unable to encode interactions to base64", e); + if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based authentication session initialisation response field 'sessionID' is missing or empty"); } } } diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 3eeb2f76..8dc64f42 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -32,11 +32,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; @@ -44,7 +45,7 @@ import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; -import ee.sk.smartid.util.NotificationUtil; +import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.StringUtil; public class NotificationSignatureSessionRequestBuilder { @@ -60,7 +61,7 @@ public class NotificationSignatureSessionRequestBuilder { private CertificateLevel certificateLevel; private String nonce; private Set capabilities; - private List allowedInteractionsOrder; + private List interactions; private Boolean shareMdClientIpAddress; private SignatureAlgorithm signatureAlgorithm; private DigestInput digestInput; @@ -157,8 +158,9 @@ public NotificationSignatureSessionRequestBuilder withCapabilities(Set c * @param allowedInteractionsOrder the allowed interactions order * @return this builder */ + @Deprecated // TODO - 17.09.25: fix in SLIB-116 public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.allowedInteractionsOrder = allowedInteractionsOrder; + this.interactions = allowedInteractionsOrder; return this; } @@ -254,7 +256,7 @@ private SignatureSessionRequest createSignatureSessionRequest() { signatureProtocolParameters, nonce, capabilities, - NotificationUtil.encodeToBase64(allowedInteractionsOrder), + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, null ); @@ -275,10 +277,12 @@ private void validateParameters() { } private void validateAllowedInteractions() { - if (allowedInteractionsOrder == null || allowedInteractionsOrder.isEmpty()) { + if (interactions == null || interactions.isEmpty()) { throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); } - allowedInteractionsOrder.forEach(Interaction::validate); + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } } private void validateResponseParameters(NotificationSignatureSessionResponse response) { diff --git a/src/test/java/ee/sk/smartid/InteractionUtil.java b/src/main/java/ee/sk/smartid/RpChallenge.java similarity index 64% rename from src/test/java/ee/sk/smartid/InteractionUtil.java rename to src/main/java/ee/sk/smartid/RpChallenge.java index 0de05015..568a6f57 100644 --- a/src/test/java/ee/sk/smartid/InteractionUtil.java +++ b/src/main/java/ee/sk/smartid/RpChallenge.java @@ -26,27 +26,30 @@ * #L% */ -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.rest.dao.Interaction; - import org.bouncycastle.util.encoders.Base64; -import java.nio.charset.StandardCharsets; -import java.util.List; - -public class InteractionUtil { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private InteractionUtil() { +/** + * Represents an RP challenge + * + * @param value a byte array of representing the challenge + */ +public record RpChallenge(byte[] value) { + + /** + * Returns a copy of the challenge value + * + * @return a byte array representing the challenge + */ + public byte[] value() { + return value.clone(); } - public static String encodeInteractionsAsBase64(List interactions) { - try { - String json = objectMapper.writeValueAsString(interactions); - return Base64.toBase64String(json.getBytes(StandardCharsets.UTF_8)); - } catch (Exception e) { - throw new RuntimeException("Failed to encode interactions to Base64", e); - } + /** + * Returns the Base64 encoded representation of the challenge value + * + * @return a Base64 encoded string representing the challenge + */ + public String toBase64EncodedValue() { + return Base64.toBase64String(value); } } diff --git a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java index 0a466a7a..d55d62d9 100644 --- a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java +++ b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,10 +28,10 @@ import java.security.SecureRandom; -import org.bouncycastle.util.encoders.Base64; +import ee.sk.smartid.exception.permanent.SmartIdClientException; /** - * Utility class for generating RP challenges in Base64 format + * Utility class for generating RP challenge */ public class RpChallengeGenerator { @@ -42,28 +42,28 @@ private RpChallengeGenerator() { } /** - * Generates a RP challenge with a maximum length of 64 bytes + * Generates an RP challenge with a maximum length of 64 bytes * - * @return RP challenge in Base64 format + * @return RP challenge */ - public static String generate() { + public static RpChallenge generate() { byte[] randBytes = new byte[MAX_LENGTH]; new SecureRandom().nextBytes(randBytes); - return Base64.toBase64String(randBytes); + return new RpChallenge(randBytes); } /** - * Generates a RP challenge with specified length + * Generates an RP challenge with specified length * * @param length length of the challenge - * @return RP challenge in Base64 format + * @return RP challenge */ - public static String generate(int length) { + public static RpChallenge generate(int length) { if (length < MIN_LENGTH || length > MAX_LENGTH) { - throw new IllegalArgumentException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); + throw new SmartIdClientException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); } byte[] randBytes = getRandomBytes(length); - return Base64.toBase64String(randBytes); + return new RpChallenge(randBytes); } private static byte[] getRandomBytes(int length) { diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index 177cb869..661658f5 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,30 +28,38 @@ import java.nio.ByteBuffer; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for calculating verification code from a hash. + */ public class VerificationCodeCalculator { - /** - * The Verification Code (VC) is computed as: - *

    - * integer(SHA256(hash)[−2:−1]) mod 10000 - *

    - * where we take SHA256 result, extract 2 rightmost bytes from it, - * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. - *

    - * SHA256 is always used here, no matter what was the algorithm used to calculate hash. - * - * @param documentHash hash used to calculate verification code. - * @return verification code. - */ - public static String calculate(byte[] documentHash) { - byte[] digest = DigestCalculator.calculateDigest(documentHash, HashAlgorithm.SHA_256); - ByteBuffer byteBuffer = ByteBuffer.wrap(digest); - int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 - int rightMostBytesIndex = byteBuffer.limit() - shortBytes; - short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); - int positiveInteger = ((int) twoRightmostBytes) & 0xffff; - String code = String.valueOf(positiveInteger); - String paddedCode = "0000" + code; - return paddedCode.substring(code.length()); - } + /** + * The Verification Code (VC) is computed as: + *

    + * integer(SHA256(data)[−2:−1]) mod 10000 + *

    + * where we take SHA256 result, extract 2 rightmost bytes from it, + * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. + *

    + * SHA256 is always used here + * + * @param data byte array to calculate verification code from + * @return verification code. + */ + public static String calculate(byte[] data) { + if (data == null || data.length == 0) { + throw new SmartIdClientException("Parameter 'data' cannot be empty"); + } + byte[] digest = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); + ByteBuffer byteBuffer = ByteBuffer.wrap(digest); + int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 + int rightMostBytesIndex = byteBuffer.limit() - shortBytes; + short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); + int positiveInteger = ((int) twoRightmostBytes) & 0xffff; + String code = String.valueOf(positiveInteger); + String paddedCode = "0000" + code; + return paddedCode.substring(code.length()); + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java b/src/main/java/ee/sk/smartid/VerificationCodeType.java similarity index 66% rename from src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java rename to src/main/java/ee/sk/smartid/VerificationCodeType.java index 38e4a644..9f5b673f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteractionFlow.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeType.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid; /*- * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,25 +26,25 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonValue; - -public enum DeviceLinkInteractionFlow implements InteractionFlow { - - DISPLAY_TEXT_AND_PIN("displayTextAndPIN"), - CONFIRMATION_MESSAGE("confirmationMessage"); +/** + * Verification code types to be used with notification-based authentication request and signing session response + */ +public enum VerificationCodeType { - private final String code; + NUMERIC4("numeric4"); - DeviceLinkInteractionFlow(String code) { - this.code = code; - } + private final String value; - @JsonValue - public String getCode() { - return code; + VerificationCodeType(String value) { + this.value = value; } - public boolean is(String typeCodeString) { - return this.getCode().equals(typeCodeString); + /** + * Returns the string representation of the verification code type. + * + * @return the string value of the verification code type + */ + public String getValue(){ + return value; } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java b/src/main/java/ee/sk/smartid/common/InteractionType.java similarity index 67% rename from src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java rename to src/main/java/ee/sk/smartid/common/InteractionType.java index 4e5dff14..722e0ff2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/InteractionFlow.java +++ b/src/main/java/ee/sk/smartid/common/InteractionType.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.common; /*- * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,9 +26,22 @@ * #L% */ -public interface InteractionFlow { +/** + * Representations of interaction types that can be used in authentication and signing requests + */ +public interface InteractionType { + /** + * Provides the interaction type as value that can be used in the Smart ID API + * + * @return code representing the interaction type + */ String getCode(); - boolean is(String typeCodeString); + /** + * Provides the maximum length of the display text for this interaction type + * + * @return maximum length of the display text + */ + int getMaxLength(); } diff --git a/src/main/java/ee/sk/smartid/common/InteractionValidator.java b/src/main/java/ee/sk/smartid/common/InteractionValidator.java new file mode 100644 index 00000000..21873f3e --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/InteractionValidator.java @@ -0,0 +1,55 @@ +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.util.StringUtil; + +/** + * Validator for interactions + */ +public final class InteractionValidator { + + private InteractionValidator() { + } + + /** + * Validates that the text is set and does not exceed the maximum length defined by the type + * + * @param type the type to be validated + * @param text the text to be validated + * @param implementation of InteractionType + */ + public static void validate(T type, String text) { + if (StringUtil.isEmpty(text)) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must be set when type is '%s'", "displayText" + type.getMaxLength(), type)); + } + if (text.length() > type.getMaxLength()) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must not exceed %d characters", "displayText" + type.getMaxLength(), type.getMaxLength())); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java new file mode 100644 index 00000000..97a9e8b7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -0,0 +1,60 @@ +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; + +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Mapper form converting between different interaction representations + */ +public final class InteractionsMapper { + + private InteractionsMapper() { + } + + /** + * Converts from any SmartIdInteraction to Interaction + * + * @param interaction the interaction to be converted + * @return interaction to be used in REST request + */ + public static Interaction from(T interaction) { + return new Interaction(interaction.type().getCode(), interaction.displayText60(), interaction.displayText200()); + } + + /** + * Converts from any list of SmartIdInteraction to list of Interaction + * + * @param interactions the interactions to be converted + * @return list of interactions to be used in REST request + */ + public static List from(List interactions) { + return interactions.stream().map(InteractionsMapper::from).toList(); + } +} diff --git a/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java new file mode 100644 index 00000000..9a4e7d22 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Interaction to be used in authentication and signing requests + */ +public interface SmartIdInteraction { + + /** + * Gets the interaction type + * + * @return the interaction type + */ + InteractionType type(); + + /** + * Gets the text to be displayed on the device screen (maximum length 60 characters). + * + * @return the text to be displayed on the device screen (maximum length 60 characters). + */ + String displayText60(); + + /** + * Gets the text to be displayed on the device screen (maximum length 200 characters). + * + * @return the text to be displayed on the device screen (maximum length 200 characters). + */ + String displayText200(); +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java new file mode 100644 index 00000000..fd3b201d --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java @@ -0,0 +1,78 @@ +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * DeviceLink interaction to be used in device-link based authentication and signing requests + * + * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record DeviceLinkInteraction(DeviceLinkInteractionType type, + String displayText60, + String displayText200) implements SmartIdInteraction { + + public DeviceLinkInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == DeviceLinkInteractionType.CONFIRMATION_MESSAGE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction displayTextAndPin(String displayText60) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction confirmationMessage(String displayText200) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } +} + diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java similarity index 65% rename from src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java rename to src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java index 2b3ccba6..17214154 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteractionFlow.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java @@ -1,10 +1,10 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.common.devicelink.interactions; /*- * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,25 +26,31 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonValue; +import ee.sk.smartid.common.InteractionType; -public enum NotificationInteractionFlow implements InteractionFlow { +/** + * Device link interaction types that can be used in device link based authentication and signing requests + */ +public enum DeviceLinkInteractionType implements InteractionType { - VERIFICATION_CODE_CHOICE("verificationCodeChoice"), - CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice"); + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + CONFIRMATION_MESSAGE("confirmationMessage", 200); private final String code; + private final int maxLength; - NotificationInteractionFlow(String code) { + DeviceLinkInteractionType(String code, int maxLength) { this.code = code; + this.maxLength = maxLength; } - @JsonValue + @Override public String getCode() { return code; } - public boolean is(String typeCodeString) { - return this.getCode().equals(typeCodeString); + @Override + public int getMaxLength() { + return maxLength; } } diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java new file mode 100644 index 00000000..6aac6a73 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java @@ -0,0 +1,88 @@ +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * Notification interaction to be used in notification based authentication and signing requests + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record NotificationInteraction(NotificationInteractionType type, + String displayText60, + String displayText200) implements Serializable, SmartIdInteraction { + + public NotificationInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == NotificationInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == NotificationInteractionType.CONFIRMATION_MESSAGE + || type == NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return the interaction + */ + public static NotificationInteraction displayTextAndPin(String displayText60) { + return new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessage(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, null, displayText200); + } +} diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java new file mode 100644 index 00000000..9e8e3c3f --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java @@ -0,0 +1,57 @@ +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionType; + +/** + * Interaction types that can be used in notification-based authentication and signing requests + */ +public enum NotificationInteractionType implements InteractionType { + + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + CONFIRMATION_MESSAGE("confirmationMessage", 200), + CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); + + private final String code; + private final int maxLength; + + NotificationInteractionType(String code, int maxLength) { + this.code = code; + this.maxLength = maxLength; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMaxLength() { + return maxLength; + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java deleted file mode 100644 index 37411a53..00000000 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedVerificationChoiceException.java +++ /dev/null @@ -1,33 +0,0 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -public class UserRefusedVerificationChoiceException extends UserRefusedException { - public UserRefusedVerificationChoiceException() { - super("User cancelled verificationCodeChoice screen"); - } -} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index aebc9d23..96805701 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -36,8 +36,9 @@ import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; @@ -148,7 +149,7 @@ public interface SmartIdConnector extends Serializable { * @param authenticationRequest The device link authentication session request * @return The device link authentication session response */ - DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest); + DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest); /** * Create authentication session with device link using semantics identifier @@ -157,7 +158,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return The device link authentication session response */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); /** * Create authentication session with device link using document number @@ -166,7 +167,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return The device link authentication session response */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); /** * Create authentication session with notification using semantics identifier @@ -175,7 +176,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return The notification authentication session response */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); /** * Create authentication session with notification using document number @@ -184,5 +185,5 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return The notification authentication session response */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber); + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber); } diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 3ddaac08..f648ffca 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -44,13 +44,14 @@ import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -136,8 +137,8 @@ public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundEx } @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - logger.debug("Starting dynamic link authentication session with semantics identifier"); + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + logger.debug("Starting device link authentication session with semantics identifier"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) .path(semanticsIdentifier.getIdentifier()) @@ -146,7 +147,7 @@ public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSess } @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { logger.debug("Starting device link authentication session with document number"); URI uri = UriBuilder.fromUri(endpointUrl) .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) @@ -156,7 +157,7 @@ public DeviceLinkSessionResponse initDeviceLinkAuthentication(AuthenticationSess } @Override - public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(AuthenticationSessionRequest authenticationRequest) { + public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest) { logger.debug("Starting anonymous device link authentication session"); URI uri = UriBuilder.fromUri(endpointUrl) .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) @@ -165,7 +166,7 @@ public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(Authentic } @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -175,7 +176,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(AuthenticationSessionRequest authenticationRequest, String documentNumber) { + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) diff --git a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index 414c1003..b2bf44c4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java @@ -28,6 +28,13 @@ import java.io.Serializable; +/** + * ACSP_V2 signature protocol parameters + * + * @param rpChallenge Required. The RP challenge in Base64 encoding + * @param signatureAlgorithm Required. The signature algorithm. Only supported value is rsassa-pss + * @param signatureAlgorithmParameters Required. The signature algorithm parameters + */ public record AcspV2SignatureProtocolParameters(String rpChallenge, String signatureAlgorithm, SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java deleted file mode 100644 index 7d228f0b..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.SignatureProtocol; - -public record AuthenticationSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - SignatureProtocol signatureProtocol, - AcspV2SignatureProtocolParameters signatureProtocolParameters, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { -} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java new file mode 100644 index 00000000..abc502a4 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java @@ -0,0 +1,57 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.SignatureProtocol; + +/** + * Device link authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Authentication signature protocol to be used + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + SignatureProtocol signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java deleted file mode 100644 index 6868084b..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkInteraction.java +++ /dev/null @@ -1,66 +0,0 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.rest.dao.DeviceLinkInteractionFlow.CONFIRMATION_MESSAGE; -import static ee.sk.smartid.rest.dao.DeviceLinkInteractionFlow.DISPLAY_TEXT_AND_PIN; - -public class DeviceLinkInteraction extends Interaction { - - public DeviceLinkInteraction() { - } - - private DeviceLinkInteraction(DeviceLinkInteractionFlow type) { - this.type = type; - } - - public static DeviceLinkInteraction displayTextAndPIN(String displayText60) { - var interaction = new DeviceLinkInteraction(DISPLAY_TEXT_AND_PIN); - interaction.displayText60 = displayText60; - return interaction; - } - - public static DeviceLinkInteraction confirmationMessage(String displayText200) { - var interaction = new DeviceLinkInteraction(CONFIRMATION_MESSAGE); - interaction.displayText200 = displayText200; - return interaction; - } - - @Override - protected void validateInteractionsDisplayText60() { - if (getType() == DISPLAY_TEXT_AND_PIN) { - validateDisplayText60(); - } - } - - @Override - protected void validateInteractionsDisplayText200() { - if (getType() == CONFIRMATION_MESSAGE) { - validateDisplayText200(); - } - } -} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index 2ca258f3..cf61e85a 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -34,14 +34,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Response of session creation for device link flows + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. + * @param deviceLinkBase Required. Base URI for generating device link + * @param receivedAt Timestamp when the response was received + */ + @JsonIgnoreProperties(ignoreUnknown = true) public record DeviceLinkSessionResponse(String sessionID, String sessionToken, String sessionSecret, URI deviceLinkBase, - Instant receivedAt - -) implements Serializable { + Instant receivedAt) implements Serializable { @JsonCreator public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index f9b4cbe3..2e84645d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,70 +27,15 @@ */ import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public abstract class Interaction { - - protected InteractionFlow type; - - protected String displayText60; - protected String displayText200; - - public InteractionFlow getType() { - return type; - } - - public void setType(DeviceLinkInteractionFlow type) { - this.type = type; - } - - public String getDisplayText60() { - return displayText60; - } - - public void setDisplayText60(String displayText60) { - this.displayText60 = displayText60; - } - public String getDisplayText200() { - return displayText200; - } - - public void setDisplayText200(String displayText200) { - this.displayText200 = displayText200; - } - - public void validate() { - validateInteractionsDisplayText60(); - validateInteractionsDisplayText200(); - } - - protected abstract void validateInteractionsDisplayText60(); - - protected abstract void validateInteractionsDisplayText200(); - - protected void validateDisplayText60() { - if (getDisplayText60() == null) { - throw new SmartIdClientException("displayText60 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText60().length() > 60) { - throw new SmartIdClientException("displayText60 must not be longer than 60 characters"); - } - if (getDisplayText200() != null) { - throw new SmartIdClientException("displayText200 must be null for AllowedInteractionOrder of type " + getType()); - } - } - - protected void validateDisplayText200() { - if (getDisplayText200() == null) { - throw new SmartIdClientException("displayText200 cannot be null for AllowedInteractionOrder of type " + getType()); - } - if (getDisplayText200().length() > 200) { - throw new SmartIdClientException("displayText200 must not be longer than 200 characters"); - } - if (getDisplayText60() != null) { - throw new SmartIdClientException("displayText60 must be null for AllowedInteractionOrder of type " + getType()); - } - } +/** + * Interaction to be used in authentication and signing requests + * + * @param type Required. The interaction type + * @param displayText60 Requirement depends on the type. The text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 Requirement depends on the type. the text to be displayed on the device screen (maximum length 200 characters). + */ +public record Interaction(String type, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText60, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText200) { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java new file mode 100644 index 00000000..316bf206 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java @@ -0,0 +1,56 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Signature protocol to be used for authentication + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param vcType Required. Verification code type to be used in the authentication session + */ +public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + String vcType) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java index ca9cbeb7..1cb13f47 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java @@ -30,26 +30,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -@JsonIgnoreProperties(ignoreUnknown = true) -public class NotificationAuthenticationSessionResponse implements Serializable { - - private String sessionID; - - private VerificationCode vc; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } - - public VerificationCode getVc() { - return vc; - } +/** + * Notification-based authentication session response + * + * @param sessionID the ID of the created authentication session + */ - public void setVc(VerificationCode verificationCode) { - this.vc = verificationCode; - } +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationAuthenticationSessionResponse(String sessionID) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java deleted file mode 100644 index 4268baf9..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java +++ /dev/null @@ -1,63 +0,0 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.rest.dao.NotificationInteractionFlow.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE; -import static ee.sk.smartid.rest.dao.NotificationInteractionFlow.VERIFICATION_CODE_CHOICE; - -public class NotificationInteraction extends Interaction { - - public NotificationInteraction(NotificationInteractionFlow notificationInteractionFlow) { - this.type = notificationInteractionFlow; - } - - public static NotificationInteraction verificationCodeChoice(String displayText60) { - var interaction = new NotificationInteraction(VERIFICATION_CODE_CHOICE); - interaction.displayText60 = displayText60; - return interaction; - } - - public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { - var interaction = new NotificationInteraction(CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE); - interaction.displayText200 = displayText200; - return interaction; - } - - @Override - protected void validateInteractionsDisplayText60() { - if (getType() == VERIFICATION_CODE_CHOICE) { - validateDisplayText60(); - } - } - - @Override - protected void validateInteractionsDisplayText200() { - if (getType() == CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { - validateDisplayText200(); - } - } -} diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index 979404d4..c4832677 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -30,5 +30,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; +/** + * Additional request properties + * + * @param shareMdClientIpAddress Set if the client's device IP address should be provided in sessions status response + */ public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index 219ad562..97ce547e 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,5 +28,11 @@ import java.io.Serializable; +/** + * Parameters for signature algorithm + * + * @param hashAlgorithm Required. The hash algorithm. + * Supported values are SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 + */ public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index e640eae6..b7580803 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java @@ -33,6 +33,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class VerificationCode implements Serializable { + @Deprecated // TODO - 16.09.25: will be removed with notification-based signature flow changes; SLIB-116 public static final String ALPHA_NUMERIC_4 = "alphaNumeric4"; private String type; diff --git a/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java b/src/main/java/ee/sk/smartid/util/InteractionUtil.java similarity index 91% rename from src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java rename to src/main/java/ee/sk/smartid/util/InteractionUtil.java index 2f0a93da..270f2b9c 100644 --- a/src/main/java/ee/sk/smartid/util/DeviceLinkUtil.java +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -38,21 +38,21 @@ /** * Utility class for interactions related actions */ -public class DeviceLinkUtil { +public class InteractionUtil { private static final ObjectMapper mapper = new ObjectMapper(); - private DeviceLinkUtil() { + private InteractionUtil() { } /** - * Encodes list of interactions to Base64 string + * Encodes list of interactions to Base64-encoded string * * @param interactions list of interactions * @return base64 encoded string * @throws SmartIdClientException if unable to encode interactions */ - public static String encodeToBase64(List interactions) { + public static String encodeToBase64(List interactions) { try { String json = mapper.writeValueAsString(interactions); return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java index 9a066db9..53bf2cf9 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java @@ -42,12 +42,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.SessionCertificate; import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; @@ -56,6 +57,7 @@ import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; class AuthenticationResponseValidatorTest { @@ -77,7 +79,7 @@ void setUp() { void validate() { String rpChallenge = ""; SessionStatus sessionStatus = new SessionStatus(); - AuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); @@ -210,14 +212,14 @@ private static SessionStatus toSessionsStatus(String certificateValue, String ce return sessionStatus; } - private static AuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { - return new AuthenticationSessionRequest( + private static DeviceLinkAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new DeviceLinkAuthenticationSessionRequest( "00000000-0000-0000-0000-000000000001", "DEMO", certificateLevel, SignatureProtocol.ACSP_V2, new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), null, null, null); diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index b41bb68b..2286a5f7 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -41,6 +41,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -60,13 +61,14 @@ import org.mockito.ArgumentCaptor; import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.SemanticsIdentifier; class DeviceLinkAuthenticationSessionRequestBuilderTest { @@ -84,53 +86,60 @@ void setUp() { class ValidateRequiredRequestParameters { @Test - void initAuthenticationSession_ok() throws Exception { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); + void initAuthenticationSession_anonymousAuthentication_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + builder.initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals("QUALIFIED", request.certificateLevel()); - assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertNotNull(request.signatureProtocolParameters().rpChallenge()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - DeviceLinkInteraction[] parsed = parseInteractionsFromBase64(request.interactions()); - assertTrue(Stream.of(parsed).anyMatch(i -> i.getType().is("displayTextAndPIN"))); + assertAuthenticationSessionRequest(request); + } + + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withDocumentNumber("PNOEE-48010010101-MOCK-Q")); + + builder.initAuthenticationSession(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + builder.initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); } @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(certificateLevel) - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + toDeviceLinkRequestBuilder(b -> b.withCertificateLevel(certificateLevel)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.certificateLevel()); } @@ -138,21 +147,15 @@ void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLeve @ParameterizedTest @EnumSource void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withSignatureAlgorithm(signatureAlgorithm) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); @@ -160,20 +163,14 @@ void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatur @Test void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + toBaseDeviceLinkRequestBuilder().initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); assertNull(request.requestProperties()); } @@ -181,243 +178,160 @@ void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_o @ParameterizedTest @ValueSource(booleans = {true, false}) void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withShareMdClientIpAddress(ipRequested) + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) .initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); assertNotNull(request.requestProperties()); assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initAuthenticationSession_capabilities_ok(String capabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(0, request.capabilities().size()); + } + @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withCapabilities(capabilities) - .initAuthenticationSession(); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); assertEquals(expectedCapabilities, request.capabilities()); } @Test void initAuthenticationSession_initialCallbackUrlIsValid_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(createDynamicLinkAuthenticationResponse()); - - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withInitialCallbackUrl("https://valid.example.com/path") - .initAuthenticationSession(); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl("https://example.com/callback")); + + builder.initAuthenticationSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - AuthenticationSessionRequest request = requestCaptor.getValue(); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals("https://valid.example.com/path", request.initialCallbackUrl()); + assertEquals("https://example.com/callback", request.initialCallbackUrl()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName("DEMO") - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName(relyingPartyName) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(rpChallenge) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals(expectedException, exception.getMessage()); } @Test void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withSignatureAlgorithm(null) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource - void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(interactions) - .initAuthenticationSession()); + void initAuthenticationSession_interactionsIsEmpty_throwException(List interactions) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); } @ParameterizedTest - @ArgumentsSource(DuplicateInteractionsProvider.class) + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(duplicateInteractions) - .initAuthenticationSession()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(duplicateInteractions)); + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); } - @ParameterizedTest - @ArgumentsSource(InvalidInteractionsProvider.class) - public void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(DeviceLinkInteraction interaction, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(interaction)) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); - } - @ParameterizedTest @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log in"))) - .withInitialCallbackUrl(url) - .initAuthenticationSession() - ); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals(expectedErrorMessage, exception.getMessage()); } @Test void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(null) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession() - ); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); } @Test void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(null) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession() - ); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); } @Test void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { - var exception = assertThrows(SmartIdRequestSetupException.class, () -> - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .initAuthenticationSession() - ); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> + b.withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); } - - private DeviceLinkInteraction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { - byte[] decodedBytes = Base64.decode(base64EncodedJson); - String json = new String(decodedBytes, StandardCharsets.UTF_8); - var mapper = new ObjectMapper(); - return mapper.readValue(json, DeviceLinkInteraction[].class); - } } @Nested @@ -426,105 +340,82 @@ class ValidateRequiredResponseParameters { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - initAuthentication(); - }); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - initAuthentication(); - }); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var dynamicLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(dynamicLinkAuthenticationSessionResponse); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - initAuthentication(); - }); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000",generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue) ); - when(connector.initAnonymousDeviceLinkAuthentication(any(AuthenticationSessionRequest.class))).thenReturn(response); - initAuthentication(); - }); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(response); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); } - - private void initAuthentication() { - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .initAuthenticationSession(); - } } @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); + void getAuthenticationSessionRequest_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + assertAuthenticationSessionRequest(request); } @Test - void initAuthenticationSession_withDocumentNumber() { - when(connector.initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), any(String.class))) - .thenReturn(createDynamicLinkAuthenticationResponse()); + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - new DeviceLinkAuthenticationSessionRequestBuilder(connector) + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Authentication session request has not been initialized yet", ex.getMessage()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseDeviceLinkRequestBuilder()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toBaseDeviceLinkRequestBuilder() { + return new DeviceLinkAuthenticationSessionRequestBuilder(connector) .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") .withRelyingPartyName("DEMO") .withRpChallenge(generateBase64String("a".repeat(32))) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPIN("Log into internet banking system"))) - .withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initDeviceLinkAuthentication(any(AuthenticationSessionRequest.class), documentNumberCaptor.capture()); - String capturedDocumentNumber = documentNumberCaptor.getValue(); - - assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPin("Log into internet banking system"))); } - private DeviceLinkSessionResponse createDynamicLinkAuthenticationResponse() { + private DeviceLinkSessionResponse toDeviceLinkAuthenticationResponse() { return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), @@ -535,67 +426,35 @@ private static String generateBase64String(String text) { return Base64.toBase64String(text.getBytes()); } - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } + private void assertAuthenticationSessionRequest(DeviceLinkAuthenticationSessionRequest request) throws Exception { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals("QUALIFIED", request.certificateLevel()); + assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertNotNull(request.signatureProtocolParameters().rpChallenge()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + + Interaction[] parsed = parseInteractionsFromBase64(request.interactions()); + assertTrue(Stream.of(parsed).anyMatch(i -> i.type().equals("displayTextAndPIN"))); } - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[0], Collections.emptySet()), - Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), - Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) - ); - } - } - - private static class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Value for 'rpChallenge' must be Base64-encoded string"), - Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(30).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters"), - Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(67).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters") - ); - } + private Interaction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { + byte[] decodedBytes = Base64.decode(base64EncodedJson); + String json = new String(decodedBytes, StandardCharsets.UTF_8); + var mapper = new ObjectMapper(); + return mapper.readValue(json, Interaction[].class); } - private static class DuplicateInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - var interaction1 = DeviceLinkInteraction.displayTextAndPIN("Log in."); - var interaction2 = DeviceLinkInteraction.displayTextAndPIN("Log in."); - - return Stream.of( - Arguments.of(List.of(interaction1, interaction1)), - Arguments.of(List.of(interaction1, interaction2)) - ); - } - } - - private static class InvalidInteractionsProvider implements ArgumentsProvider { + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(Named.of("provided text is null", DeviceLinkInteraction.displayTextAndPIN(null)), - "displayText60 cannot be null for AllowedInteractionOrder of type DISPLAY_TEXT_AND_PIN"), - Arguments.of(Named.of("provided text is longer than allowed 60", DeviceLinkInteraction.displayTextAndPIN("a".repeat(61))), - "displayText60 must not be longer than 60 characters"), - Arguments.of(Named.of("provided text is null", DeviceLinkInteraction.confirmationMessage(null)), - "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE"), - Arguments.of(Named.of("provided text is longer than allowed 200", DeviceLinkInteraction.confirmationMessage("a".repeat(201))), - "displayText200 must not be longer than 200 characters") + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") ); } } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 3b74c25b..3bd6f586 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -56,11 +56,11 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @@ -192,7 +192,7 @@ void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { @ParameterizedTest @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignablData(HashAlgorithm hashAlgorithm) { + void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); @@ -244,7 +244,7 @@ void initSignatureSession_withCapabilities(String[] capabilities, Set ex } @Test - void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { + void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); @@ -258,6 +258,31 @@ void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } + @Test + void getSignatureSessionRequest_ok() { + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + SignatureSessionRequest signatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); + assertNotNull(signature); + + assertEquals("test-relying-party-uuid", signatureSessionRequest.relyingPartyUUID()); + assertEquals("DEMO", signatureSessionRequest.relyingPartyName()); + assertEquals("RAW_DIGEST_SIGNATURE", signatureSessionRequest.signatureProtocol()); + assertNotNull(signatureSessionRequest.signatureProtocolParameters()); + assertNotNull(signatureSessionRequest.interactions()); + } + + @Test + void getSignatureSessionRequest_sessionNotStarted_throwException() { + when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); + assertEquals("Signature session has not been initiated yet", ex.getMessage()); + } + @Nested class ErrorCases { @@ -267,7 +292,7 @@ void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String doc var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed.", ex.getMessage()); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed", ex.getMessage()); } @Test @@ -338,7 +363,7 @@ void initSignatureSession_whenInteractionsIsNullOrEmpty(List duplicateInteractions) { var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); @@ -444,7 +469,7 @@ private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestB .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document"))) .withSignableData(new SignableData("Test data".getBytes())); } @@ -483,16 +508,4 @@ public Stream provideArguments(ExtensionContext context) { ); } } - - static class DuplicateInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - var interaction1 = DeviceLinkInteraction.displayTextAndPIN("Sign this."); - var interaction2 = DeviceLinkInteraction.displayTextAndPIN("Sign this again."); - return Stream.of( - Arguments.of(List.of(interaction1, interaction1)), - Arguments.of(List.of(interaction1, interaction2)) - ); - } - } } diff --git a/src/main/java/ee/sk/smartid/util/NotificationUtil.java b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java similarity index 59% rename from src/main/java/ee/sk/smartid/util/NotificationUtil.java rename to src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java index e632801f..36285640 100644 --- a/src/main/java/ee/sk/smartid/util/NotificationUtil.java +++ b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid; /*- * #%L @@ -26,25 +26,25 @@ * #L% */ -import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.List; +import java.util.stream.Stream; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.NotificationInteraction; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; -public class NotificationUtil { +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; - private static final ObjectMapper mapper = new ObjectMapper(); +public class DuplicateDeviceLinkInteractionsProvider implements ArgumentsProvider { - public static String encodeToBase64(List interactions) { - try { - String json = mapper.writeValueAsString(interactions); - return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { - throw new SmartIdClientException("Unable to encode interactions to base64", e); - } + @Override + public Stream provideArguments(ExtensionContext context) { + var interaction1 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + var interaction2 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + + return Stream.of( + Arguments.of(List.of(interaction1, interaction1)), + Arguments.of(List.of(interaction1, interaction2)) + ); } } diff --git a/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java new file mode 100644 index 00000000..b3d305cd --- /dev/null +++ b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java @@ -0,0 +1,49 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; + +public class DuplicateNotificationInteractionArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + List.of(NotificationInteraction.displayTextAndPin("Enter your PIN."), + NotificationInteraction.displayTextAndPin("Enter your PIN.")), + List.of(NotificationInteraction.displayTextAndPin("Provide your PIN"), + NotificationInteraction.displayTextAndPin("Enter your PIN."))) + .map(Arguments::of); + } +} diff --git a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java index ec60e1f6..4e48ca24 100644 --- a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java +++ b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java @@ -46,7 +46,7 @@ class ErrorResultHandlerTest { @Test void handle_nullInput() { var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); - assertEquals("Session end result is not provided", smartIdClientException.getMessage()); + assertEquals("Parameter 'sessionResult' is not provided", smartIdClientException.getMessage()); } @ParameterizedTest diff --git a/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java new file mode 100644 index 00000000..fabc18c2 --- /dev/null +++ b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java @@ -0,0 +1,50 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.bouncycastle.util.encoders.Base64.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Value for 'rpChallenge' must be Base64-encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", toBase64String("a".repeat(30).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters"), + Arguments.of(Named.of("provided value sizes exceeds max range value", toBase64String("a".repeat(67).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters") + ); + } +} diff --git a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java index 65029467..2e817836 100644 --- a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -48,10 +48,10 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; @@ -85,7 +85,7 @@ void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel cer .withDocumentNumber(DOCUMENT_NUMBER) .withSignableData(new SignableData("Test data".getBytes())) .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))); + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); LinkedSignatureSessionResponse response = builder.initSignatureSession(); @@ -222,11 +222,11 @@ void initSignatureSession_interactionsInEmpty_throwException(List interactions) { var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> - b.withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"), - DeviceLinkInteraction.displayTextAndPIN("Sign again?")))); + b.withInteractions(interactions)); var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); @@ -253,6 +253,6 @@ private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificatio .withDocumentNumber(DOCUMENT_NUMBER) .withSignableData(new SignableData("Test data".getBytes())) .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))); + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); } } diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index 5c7b3964..6ebd8297 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -37,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.bouncycastle.util.encoders.Base64; @@ -51,18 +52,16 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; -import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; class NotificationAuthenticationSessionRequestBuilderTest { @@ -73,252 +72,234 @@ void setUp() { connector = mock(SmartIdConnector.class); } - @Nested - class ValidateRequiredRequestParameters { + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - @Test - void initAuthenticationSession_ok() { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - } + builder.initAuthenticationSession(); - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(certificateLevel) - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withSignatureAlgorithm(signatureAlgorithm) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); - } + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + } - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withShareMdClientIpAddress(ipRequested) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - } + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber(null).withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withCapabilities(capabilities) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(AuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - AuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } + builder.initAuthenticationSession(); - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(createNotificationAuthenticationResponse("alphaNumeric4", "4927")); - - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationAuthentication(any(AuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingProvided_ok(boolean ipRequested) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_hashAlgorithm_ok(HashAlgorithm expectedHashAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(expectedHashAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedHashAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + + @Nested + class ValidateRequiredRequestParameters { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName(relyingPartyName) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource - void initAuthenticationSession_randomChallengeIsEmpty_throwException(String randomChallenge) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - assertEquals("Parameter randomChallenge must be set", exception.getMessage()); + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); } @ParameterizedTest - @ArgumentsSource(InvalidRandomChallengeArgumentProvider.class) - void initAuthenticationSession_randomChallengeIsInvalid_throwException(String randomChallenge, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(randomChallenge) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); assertEquals(expectedException, exception.getMessage()); } @Test void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withSignatureAlgorithm(null) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - assertEquals("Parameter signatureAlgorithm must be set", exception.getMessage()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); } - @ParameterizedTest - @ArgumentsSource(InvalidNonceProvider.class) - void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withNonce(invalidNonce) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); + @Test + void initAuthenticationSession_hashAlgorithmIsSetToNull_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource - void initAuthenticationSession_allowedInteractionsOrderIsEmpty_throwException(List interactions) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(interactions) - .initAuthenticationSession()); - assertEquals("Parameter allowedInteractionsOrder must be set", exception.getMessage()); + void initAuthenticationSession_interactionsAreEmpty_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); } @ParameterizedTest - @ArgumentsSource(InvalidInteractionsProvider.class) - void initAuthenticationSession_allowedInteractionsOrderIsInvalid_throwException(NotificationInteraction interaction, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(interaction)) - .initAuthenticationSession()); - assertEquals(expectedException, exception.getMessage()); + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); } @Test void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .initAuthenticationSession()); - - assertEquals("Either documentNumber or semanticsIdentifier must be set.", exception.getMessage()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_documentNumberAndSemanticIdentifierAreBothProvided_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); } } @@ -328,105 +309,30 @@ class ValidateRequiredResponseParameters { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); - notificationAuthenticationSessionResponse.setSessionID(sessionId); - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - }); - assertEquals("Session ID is missing from the response", exception.getMessage()); - } - - @ParameterizedTest - @NullSource - void initAuthenticationSession_vcIsNotPresentInTheResponse_throwException(VerificationCode vc) { - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> { - var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); - notificationAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); - notificationAuthenticationSessionResponse.setVc(vc); - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession(); - }); - assertEquals("VC object is missing from the response", exception.getMessage()); - } - - @Test - void initAuthenticationSession_missingVcType_throwException() { - var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse(null, "4927"); - - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession()); + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(sessionId); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - assertEquals("VC type is missing from the response", exception.getMessage()); - } - - @Test - void initAuthenticationSession_unsupportedVcType_throwException() { - var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse("numeric8", "4927"); - - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession()); - - assertEquals("Unsupported VC type: numeric8", exception.getMessage()); - } - - @Test - void initAuthenticationSession_missingVcValue_throwException() { - var notificationAuthenticationSessionResponse = createNotificationAuthenticationResponse("alphaNumeric4", null); - - when(connector.initNotificationAuthentication(any(AuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> - new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRandomChallenge(generateBase64String("a".repeat(32))) - .withAllowedInteractionsOrder(Collections.singletonList(NotificationInteraction.verificationCodeChoice("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .initAuthenticationSession()); - - assertEquals("VC value is missing from the response", exception.getMessage()); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Notification-based authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); } } - private NotificationAuthenticationSessionResponse createNotificationAuthenticationResponse(String vcType, String vcValue) { - var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(); - notificationAuthenticationSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); + } - var verificationCode = new VerificationCode(); - verificationCode.setType(vcType); - verificationCode.setValue(vcValue); + private NotificationAuthenticationSessionRequestBuilder toBaseNotificationAuthenticationSessionRequestBuilder() { + return new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withInteractions(Collections.singletonList(NotificationInteraction.displayTextAndPin("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q"); + } - notificationAuthenticationSessionResponse.setVc(verificationCode); - return notificationAuthenticationSessionResponse; + private NotificationAuthenticationSessionResponse toNotificationAuthenticationResponse() { + return new NotificationAuthenticationSessionResponse("00000000-0000-0000-0000-000000000000"); } private static String generateBase64String(String text) { @@ -443,55 +349,4 @@ public Stream provideArguments(ExtensionContext context) { ); } } - - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[0], Collections.emptySet()), - Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), - Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) - ); - } - } - - private static class InvalidRandomChallengeArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Parameter randomChallenge is not a valid Base64 encoded string"), - Arguments.of(Named.of("provided value sizes is less than allowed", Base64.toBase64String("a".repeat(31).getBytes())), - "Size of parameter randomChallenge must be between 32 and 64 bytes"), - Arguments.of(Named.of("provided value sizes exceeds max range value", Base64.toBase64String("a".repeat(65).getBytes())), - "Size of parameter randomChallenge must be between 32 and 64 bytes") - ); - } - } - - private static class InvalidNonceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), - Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") - ); - } - } - - private static class InvalidInteractionsProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided text is null", NotificationInteraction.verificationCodeChoice(null)), - "displayText60 cannot be null for AllowedInteractionOrder of type VERIFICATION_CODE_CHOICE"), - Arguments.of(Named.of("provided text is longer than allowed 60", NotificationInteraction.verificationCodeChoice("a".repeat(61))), - "displayText60 must not be longer than 60 characters"), - Arguments.of(Named.of("provided text is null", NotificationInteraction.confirmationMessageAndVerificationCodeChoice(null)), - "displayText200 cannot be null for AllowedInteractionOrder of type CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE"), - Arguments.of(Named.of("provided text is longer than allowed 200", NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))), - "displayText200 must not be longer than 200 characters") - ); - } - } } diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index e421218a..2def28ae 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -55,10 +55,10 @@ import org.junit.jupiter.params.provider.NullSource; import org.mockito.ArgumentCaptor; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @@ -77,7 +77,7 @@ void setUp() { builder = new NotificationSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID("test-relying-party-uuid") .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) .withSignableData(new SignableData("Test data".getBytes())); } diff --git a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java index 216e9df9..79ddd848 100644 --- a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,39 +30,40 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + class RpChallengeGeneratorTest { @Test void generate_defaultValueUsed() { - String challenge = RpChallengeGenerator.generate(); + RpChallenge challenge = RpChallengeGenerator.generate(); assertNotNull(challenge); - byte[] decodeChallenge = Base64.decode(challenge); - assertEquals(64, decodeChallenge.length); + assertEquals(64, challenge.value().length); } @ParameterizedTest @ValueSource(ints = {32, 43, 59, 64}) void generate_providedValuesAreInAllowedRange(int allowedValue) { - String challenge = RpChallengeGenerator.generate(allowedValue); + RpChallenge challenge = RpChallengeGenerator.generate(allowedValue); + assertNotNull(challenge); - byte[] decodeChallenge = Base64.decode(challenge); - assertEquals(allowedValue, decodeChallenge.length); + assertEquals(allowedValue, challenge.value().length); } @Test void generate_providedValueIsLessThanAllowed_throwException() { - assertThrows(IllegalArgumentException.class, () -> RpChallengeGenerator.generate(31)); + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(31)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); } @Test void generate_providedValueIsMoreThanAllowed_throwException() { - assertThrows(IllegalArgumentException.class, () -> RpChallengeGenerator.generate(65)); + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(65)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); } - } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 47c546f5..56988a37 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -46,14 +46,14 @@ import org.junit.jupiter.params.provider.EnumSource; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; @@ -163,13 +163,13 @@ class DeviceLinkAuthenticationSession { @Test void createDeviceLinkAuthentication_anonymous() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .initAuthenticationSession(); assertNotNull(response.sessionID()); @@ -182,14 +182,14 @@ void createDeviceLinkAuthentication_anonymous() { @Test void createDeviceLinkAuthentication_withDocumentNumber() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withDocumentNumber(DOCUMENT_NUMBER) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .initAuthenticationSession(); assertNotNull(response.sessionID()); @@ -202,14 +202,14 @@ void createDeviceLinkAuthentication_withDocumentNumber() { @Test void createDeviceLinkAuthentication_withSemanticsIdentifier() { SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .initAuthenticationSession(); assertNotNull(response.sessionID()); @@ -234,7 +234,7 @@ void createDeviceLinkSignature_withDocumentNumberSameDevice() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); @@ -256,7 +256,7 @@ void createDeviceLinkSignature_withDocumentNumberQrCode() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -276,7 +276,7 @@ void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { var signableHash = new SignableHash("a".repeat(32).getBytes()); DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); @@ -298,7 +298,7 @@ void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -345,7 +345,6 @@ void getCertificateByDocumentNumber_withUnknownState_throwsException() { } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18089) class NotificationAuthenticationSession { @@ -353,37 +352,31 @@ class NotificationAuthenticationSession { @Test void createNotificationAuthentication_withSemanticsIdentifier() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", - "requests/auth/notification/notification-authentication-session-request.json", - "responses/notification-session-response.json"); + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) .initAuthenticationSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertNotNull(response.sessionID()); } @Test void createNotificationAuthentication_withDocumentNumber() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/auth/notification/notification-authentication-session-request.json", - "responses/notification-session-response.json"); + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() .withDocumentNumber(DOCUMENT_NUMBER) - .withRandomChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) .initAuthenticationSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertNotNull(response.sessionID()); } } @@ -402,7 +395,7 @@ void createNotificationSignature_withDocumentNumber() { NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withDocumentNumber(DOCUMENT_NUMBER) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.confirmationMessage("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -424,7 +417,7 @@ void createNotificationSignature_withSemanticsIdentifier() { .withRelyingPartyName("DEMO") .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))) + .withAllowedInteractionsOrder(List.of(NotificationInteraction.confirmationMessage("Verify the code"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -452,7 +445,7 @@ void createLinkedNotificationSignature_onlyRequiredFields_ok() { .withSignableData(new SignableData("Test data".getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withLinkedSessionID("10000000-0000-000-000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) .initSignatureSession(); assertNotNull(response); @@ -471,7 +464,7 @@ void createLinkedNotificationSignature_allFields_ok() { .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) .withLinkedSessionID("10000000-0000-000-000-000000000000") .withNonce("cmFuZG9tTm9uY2U=") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) .withShareMdClientIpAddress(true) .initSignatureSession(); @@ -512,17 +505,17 @@ class DynamicContentForAuth { @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .withHashAlgorithm(HashAlgorithm.SHA3_512) .withInitialCallbackUrl(INITIAL_CALLBACK_URL); DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - AuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); URI deviceLink = smartIdClient.createDynamicContent() .withSchemeName("smart-id-demo") @@ -542,16 +535,16 @@ void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType device @Test void createDynamicContent_authenticationWithQRCode() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .withHashAlgorithm(HashAlgorithm.SHA3_512); DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); @@ -573,16 +566,16 @@ void createDynamicContent_authenticationWithQRCode() { @Test void createDynamicContent_authenticationWithQRCodeImage() { SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) .withHashAlgorithm(HashAlgorithm.SHA3_512); DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); URI qrCodeUri = smartIdClient.createDynamicContent() @@ -620,7 +613,7 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .initSignatureSession(); @@ -649,7 +642,7 @@ void createDynamicContent_withQrCode() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); @@ -679,7 +672,7 @@ void createDynamicContent_withQrCodeImage() { DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign document?"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) .initSignatureSession(); diff --git a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java index 597dae6a..47e0860d 100644 --- a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java @@ -35,7 +35,6 @@ import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedVerificationChoiceException; public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { @@ -44,7 +43,6 @@ public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), - Arguments.of("verificationCodeChoice", UserRefusedVerificationChoiceException.class), Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); } } diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index 986a9e61..847e2fa7 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -28,33 +28,56 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; public class VerificationCodeCalculatorTest { @Test - public void getVerificationCode() { + public void calculate_ok() { byte[] dummyDocumentHash = new byte[]{27, -69}; String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); assertEquals("4555", verificationCode); } - @Test - public void calculateCorrectVerificationCode() { - assertVerificationCode("7712", "Hello World!"); - assertVerificationCode("4612", "Hedgehogs – why can't they just share the hedge?"); - assertVerificationCode("7782", "Go ahead, make my day."); - assertVerificationCode("1464", "You're gonna need a bigger boat."); - assertVerificationCode("4240", "Say 'hello' to my little friend!"); + @ParameterizedTest + @ArgumentsSource(VerificationCodeCalculatorArgumentProvider.class) + public void calculate_generateCorrectVerificationCodes(String expectedVerificationCode, String inputString) { + byte[] hash = DigestCalculator.calculateDigest(inputString.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + assertEquals(expectedVerificationCode, VerificationCodeCalculator.calculate(hash)); } - private void assertVerificationCode(String verificationCode, String dataString) { - byte[] data = dataString.getBytes(StandardCharsets.UTF_8); - byte[] hash = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); - assertEquals(verificationCode, VerificationCodeCalculator.calculate(hash)); + @ParameterizedTest + @NullAndEmptySource + public void calculate_withEmptyInput_throwsException(byte[] data) { + var ex = assertThrows(SmartIdClientException.class, () -> VerificationCodeCalculator.calculate(data)); + assertEquals("Parameter 'data' cannot be empty", ex.getMessage()); + } + + private static class VerificationCodeCalculatorArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("7712", "Hello World!"), + Arguments.of("4612", "Hedgehogs – why can't they just share the hedge?"), + Arguments.of("7782", "Go ahead, make my day."), + Arguments.of("1464", "You're gonna need a bigger boat."), + Arguments.of("4240", "Say 'hello' to my little friend!") + ); + } } } diff --git a/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java new file mode 100644 index 00000000..03730941 --- /dev/null +++ b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java @@ -0,0 +1,73 @@ +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class InteractionValidatorTest { + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_deviceLinkInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_notificationInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getInvalidConfirmationMessageDisplayText") + void validate_interactionWithInvalidDisplayTextLength_throwException(String displayText, String expectedMessage) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> InteractionValidator.validate(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, displayText)); + assertEquals(expectedMessage, ex.getMessage()); + } + + public static Stream getValidDisplayTextForInteraction() { + return Stream.of("a", "a".repeat(60)).map(Arguments::of); + } + + public static Stream getInvalidConfirmationMessageDisplayText() { + return Stream.of(Arguments.of(null, "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("", "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("a".repeat(201), "Value for 'displayText200' must not exceed 200 characters")); + } +} diff --git a/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java new file mode 100644 index 00000000..286c4c54 --- /dev/null +++ b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java @@ -0,0 +1,88 @@ +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.rest.dao.Interaction; + +class InteractionsMapperTest { + + @Test + void from_deviceLinkInteraction() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_deviceLinkInteractionsList() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteraction() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteractionsList() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java new file mode 100644 index 00000000..b5378658 --- /dev/null +++ b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java @@ -0,0 +1,100 @@ +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class DeviceLinkInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPin("Log in?"); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.confirmationMessage("Log in?"); + + assertEquals(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateDeviceLinkWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java new file mode 100644 index 00000000..eb9e3874 --- /dev/null +++ b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java @@ -0,0 +1,125 @@ +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class NotificationInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + NotificationInteraction interaction = NotificationInteraction.displayTextAndPin("Log in?"); + + assertEquals(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessage("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessageAndVerificationCodeChoice { + + @Test + void confirmationMessageAndVerificationCodeChoice_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessageAndVerificationCodeChoice_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'", ex.getMessage()); + } + + @Test + void confirmationMessageAndVerificationCodeChoice_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateNotificationInteractionWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new NotificationInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 531446d2..09cc67ab 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -60,10 +60,12 @@ import ee.sk.smartid.CertificateValidator; import ee.sk.smartid.CertificateValidatorImpl; import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.FileTrustedCAStoreBuilder; import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.QrCodeGenerator; +import ee.sk.smartid.RpChallenge; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; import ee.sk.smartid.SignableData; @@ -74,18 +76,19 @@ import ee.sk.smartid.SmartIdClient; import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.TrustedCACertStore; +import ee.sk.smartid.VerificationCodeCalculator; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.DeviceLinkUtil; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; @Disabled("Replace relying party UUID and name with your own values in setup") @SmartIdDemoIntegrationTest @@ -116,7 +119,7 @@ class Authentication { @Test void anonymousAuthentication_withApp2App() { // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -127,13 +130,13 @@ void anonymousAuthentication_withApp2App() { .withRpChallenge(rpChallenge) .withInitialCallbackUrl("https://example.com/callback") .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Log in?") )); // Init authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status String sessionId = authenticationSessionResponse.sessionID(); @@ -142,7 +145,7 @@ void anonymousAuthentication_withApp2App() { // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.sessionSecret(); URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in dynamic link and in authCode + // Will be used to calculate elapsed time being used in device link and in authCode Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: @@ -159,6 +162,7 @@ void anonymousAuthentication_withApp2App() { .withDigest(rpChallenge) .withLang("est") .withInitialCallbackUrl("https://example.com/callback") + .withInteractions(authenticationSessionRequest.interactions()) .buildDeviceLink(sessionSecret); // Use the sessionId from the authentication session response to poll for session status updates @@ -190,8 +194,8 @@ void authentication_withSemanticIdentifierAndQrCode() { SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) - // For security reasons a new random challenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); // Store generated rpChallenge only backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -200,14 +204,14 @@ void authentication_withSemanticIdentifierAndQrCode() { .withSemanticsIdentifier(semanticsIdentifier) .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Log in?") )); // Init authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // Use sessionID to start polling for session status String sessionId = authenticationSessionResponse.sessionID(); @@ -216,7 +220,7 @@ void authentication_withSemanticIdentifierAndQrCode() { // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = authenticationSessionResponse.sessionSecret(); URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in dynamic link and in authCode + // Will be used to calculate elapsed time being used in device link Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); // Next steps: @@ -234,6 +238,7 @@ void authentication_withSemanticIdentifierAndQrCode() { .withSessionToken(sessionToken) .withDigest(rpChallenge) .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) .withLang("est") .buildDeviceLink(sessionSecret); // Return URI to be used with QR-code generation library on the frontend side @@ -263,8 +268,8 @@ void authentication_withSemanticIdentifierAndQrCode() { void authentication_withDocumentNumberAndQrCode() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; - // For security reasons a new random challenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication session status OK response @@ -273,13 +278,13 @@ void authentication_withDocumentNumberAndQrCode() { .withDocumentNumber(documentNumber) .withRpChallenge(rpChallenge) .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPIN("Log in?") + DeviceLinkInteraction.displayTextAndPin("Log in?") )); // Init authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); // Get AuthenticationSessionRequest after the request is made and store for validations - AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); String sessionId = authenticationSessionResponse.sessionID(); // SessionID is used to query sessions status later @@ -300,6 +305,7 @@ void authentication_withDocumentNumberAndQrCode() { .withDigest(rpChallenge) .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) .withLang("est") .buildDeviceLink(sessionSecret); // Return URI to be used with QR-code generation library on the frontend side @@ -344,14 +350,16 @@ void signature_withDocumentNumberAndQRCode() { // Create the signable data from DataToSign var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - // Build the dynamic link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document")); - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + var deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QSCD) .withSignableData(signableData) .withDocumentNumber(documentNumber) - .withInteractions(signatureInteractions) - .initSignatureSession(); + .withInteractions(signatureInteractions); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + SignatureSessionRequest signatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); // Process the signature response String signatureSessionId = signatureSessionResponse.sessionID(); @@ -361,13 +369,14 @@ void signature_withDocumentNumberAndQRCode() { Instant receivedAt = signatureSessionResponse.receivedAt(); URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); - // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse // Start querying sessions status // Calculate elapsed seconds from response received time long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); // Generate auth code URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") .withDeviceLinkBase(deviceLinkBase.toString()) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.SIGNATURE) @@ -375,7 +384,7 @@ void signature_withDocumentNumberAndQRCode() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") - .withInteractions(DeviceLinkUtil.encodeToBase64(signatureInteractions)) + .withInteractions(signatureSessionRequest.interactions()) .buildDeviceLink(sessionSecret); // Return URI to be used with QR-code generation library on the frontend side @@ -447,14 +456,18 @@ void signature_withSemanticIdentifier() { SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) - // Build the dynamic link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPIN("Please sign the document")); - DeviceLinkSessionResponse signatureSessionResponse = smartIdClient.createDeviceLinkSignature() + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + DeviceLinkSignatureSessionRequestBuilder deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() .withCertificateLevel(CertificateLevel.QUALIFIED) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(signatureInteractions) - .initSignatureSession(); + .withInteractions(signatureInteractions); + + // Init signature session + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + SignatureSessionRequest signatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); // Process the signature response String signatureSessionId = signatureSessionResponse.sessionID(); @@ -464,7 +477,7 @@ void signature_withSemanticIdentifier() { String sessionSecret = signatureSessionResponse.sessionSecret(); Instant receivedAt = signatureSessionResponse.receivedAt(); - // Generate QR-code or dynamic link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse // Start querying sessions status // Calculate elapsed seconds from response received time @@ -478,7 +491,7 @@ void signature_withSemanticIdentifier() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") - .withInteractions(DeviceLinkUtil.encodeToBase64(signatureInteractions)) + .withInteractions(signatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request .buildDeviceLink(sessionSecret); // Display QR-code to the user @@ -516,30 +529,29 @@ class NotificationBasedExamples { void authentication_withDocumentNumber() { String documentNumber = "PNOLT-40504040001-MOCK-Q"; - // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); + // For security reasons a new rpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient .createNotificationAuthentication() .withDocumentNumber(documentNumber) - .withRandomChallenge(rpChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") - )) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))) .initAuthenticationSession(); - String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later - - String verificationCode = authenticationSessionResponse.getVc().getValue(); - // Display the verification code to the user for confirmation + String sessionId = authenticationSessionResponse.sessionID(); // Get the session status poller SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get sessionID from current session response + // Use sessionID from current session response to poll for session status SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); assertEquals("COMPLETE", sessionStatus.getState()); @@ -550,7 +562,7 @@ void authentication_withDocumentNumber() { TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: authentication request will be fixed with notification-based authentication changes + .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: authentication request will be fixed with notification-based authentication changes, fix in SLIB-110 assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -567,30 +579,29 @@ void authentication_withSemanticIdentifier() { SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) - // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate(); + // For security reasons a new RpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient .createNotificationAuthentication() .withSemanticsIdentifier(semanticIdentifier) - .withRandomChallenge(rpChallenge) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withAllowedInteractionsOrder(Collections.singletonList( - NotificationInteraction.verificationCodeChoice("Log in?") - )) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))) .initAuthenticationSession(); - String sessionId = authenticationSessionResponse.getSessionID(); // SessionID is used to query sessions status later - - String verificationCode = authenticationSessionResponse.getVc().getValue(); - // Display the verification code to the user for confirmation + String sessionId = authenticationSessionResponse.sessionID(); // Get the session status poller SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get sessionID from current session response + // Use sessionID from current session response to poll for session status SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); assertEquals("COMPLETE", sessionStatus.getState()); @@ -600,7 +611,7 @@ void authentication_withSemanticIdentifier() { TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: will be fixed with notification-based authentication changes + .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: will be fixed with notification-based authentication changes, fix in SLIB-110 assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -690,8 +701,9 @@ void signature_withSemanticsIdentifier() { .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) .withAllowedInteractionsOrder(List.of( - NotificationInteraction.verificationCodeChoice("Please sign the document")) + NotificationInteraction.confirmationMessage("Please sign the document")) ) + .withNonce("random") .initSignatureSession(); // Process the querying sessions status response @@ -760,7 +772,7 @@ void signing_withQrCode() { // Store sessionSecret only on backend side. Do not expose it to the client side. String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in dynamic link and in authCode + // Will be used to calculate elapsed time being used in device link and in authCode Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); // Build the device link URI @@ -800,7 +812,7 @@ void signing_withQrCode() { .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) .withLinkedSessionID(certificateChoiceSessionId) .withSignableData(signableData) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign it!"))) .initSignatureSession(); // Use sessionId to poll for signature session status updates diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 2d9cf819..2ed6728c 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -41,18 +41,19 @@ import ee.sk.smartid.DigestCalculator; import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SignatureAlgorithm; import ee.sk.smartid.SignatureProtocol; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.SmartIdRestConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -61,6 +62,7 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.util.InteractionUtil; @Disabled("Relying party demo account not yet available for v3") @SmartIdDemoIntegrationTest @@ -91,7 +93,7 @@ class Authentication { @Test void initAnonymousDeviceLinkAuthentication() { - AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); @@ -103,7 +105,7 @@ void initAnonymousDeviceLinkAuthentication() { @Test void initDeviceLinkAuthentication_withDocumentNumber() { - AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); @@ -115,7 +117,7 @@ void initDeviceLinkAuthentication_withDocumentNumber() { @Test void initDeviceLinkAuthentication_withSemanticsIdentifier() { - AuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); @@ -125,18 +127,18 @@ void initDeviceLinkAuthentication_withSemanticsIdentifier() { assertNotNull(sessionResponse.receivedAt()); } - private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate(), + RpChallengeGenerator.generate().toBase64EncodedValue(), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new AuthenticationSessionRequest(RELYING_PARTY_UUID, + return new DeviceLinkAuthenticationSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, "QUALIFIED", SignatureProtocol.ACSP_V2, signatureParameters, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), null, null, null); @@ -183,7 +185,7 @@ void initDeviceLinkSignature_withSemanticIdentifier() { signatureProtocolParameters, null, null, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), null, null ); @@ -209,7 +211,7 @@ void initDeviceLinkSignature_withDocumentNumber() { signatureProtocolParameters, null, null, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), null, null ); @@ -236,9 +238,7 @@ void initNotificationAuthentication_withSemanticIdentifier() { NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); - assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); } @Test @@ -247,26 +247,24 @@ void initNotificationAuthentication_withDocumentNumber() { NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, "PNOEE-40504040001-MOCK-Q"); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); - assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); } - private static AuthenticationSessionRequest toAuthenticationRequest() { + private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate(), + RpChallengeGenerator.generate().toBase64EncodedValue(), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new AuthenticationSessionRequest(RELYING_PARTY_UUID, + return new NotificationAuthenticationSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, "QUALIFIED", - SignatureProtocol.ACSP_V2, + SignatureProtocol.ACSP_V2.name(), signatureParameters, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), new RequestProperties(true), null, - null + "numeric4" ); } } @@ -321,7 +319,7 @@ private static SignatureSessionRequest toSignatureSessionRequest() { signatureProtocolParameters, null, null, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign it!"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), null, null ); diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 8212a726..0973026c 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -58,9 +58,10 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import ee.sk.smartid.CertificateLevel; import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.InteractionUtil; import ee.sk.smartid.SignatureProtocol; import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; import ee.sk.smartid.exception.permanent.ServerMaintenanceException; @@ -69,17 +70,17 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkInteraction; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationInteraction; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; @@ -90,6 +91,7 @@ import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.util.InteractionUtil; class SmartIdRestConnectorTest { @@ -326,6 +328,7 @@ private static void assertSignatureAlgorithmParameters(SessionSignature sessionS class SemanticsIdentifierDeviceLinkAuthentication { private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-30303039914"); private SmartIdRestConnector connector; @@ -335,30 +338,109 @@ void setUp() { } @Test - void initDeviceLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + void initDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-30303039914")); + var deviceLinkAuthenticationSessionRequest = toQrAuthenticationSessionRequest(); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test - void initDeviceLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json"); - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-48010010101")); - }); + void initDeviceLinkAuthentication_sameDeviceOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(null, "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } @Test - void initDeviceLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/device-link-authentication-session-request.json"); - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), new SemanticsIdentifier("PNOEE-30303039914")); - }); + void initDeviceLinkAuthentication_sameDeviceAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(new RequestProperties(true), "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); } } @@ -367,6 +449,7 @@ void initDeviceLinkAuthentication_requestIsUnauthorized_throwException() { class DocumentNumberDeviceLinkAuthentication { private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; private SmartIdRestConnector connector; @@ -378,31 +461,78 @@ void setUp() { @Test void initDeviceLinkAuthentication() { SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/device-link-authentication-session-request.json", - "responses/device-link-authentication-session-response.json"); + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-30303039914-MOCK-Q"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); } @Test void initDeviceLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/device-link-authentication-session-request.json"); - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-48010010101-MOCK-Q"); - }); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), "PNOEE-48010010101-MOCK-Q")); } @Test - void initDeviceLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/device-link-authentication-session-request.json"); - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(), "PNOEE-30303039914-MOCK-Q"); - }); + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); } } @@ -420,34 +550,96 @@ void setUp() { } @Test - void initAnonymousDeviceLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json", "responses/device-link-authentication-session-response.json"); + void initAnonymousDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initAnonymousDeviceLinkAuthentication_sameDeviceFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); Instant end = Instant.now(); assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); } + @Test + void initAnonymousDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + @Test void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json"); - connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); - }); + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); } @Test void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/device-link-authentication-session-request.json"); - connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest()); - }); + SmartIdRestServiceStubs.stubUnauthorizedResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18082) class SemanticsIdentifierNotificationAuthentication { @@ -462,34 +654,95 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18082"); } - @Disabled("Request body has changed") @Test - void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); + void initNotificationAuthentication_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), SEMANTICS_IDENTIFIER); assertNotNull(response); } + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + @Test void initNotificationAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); - }); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); } - @Disabled("Request body has changed") @Test - void initNotificationAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER); - }); + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-109") @Nested @WireMockTest(httpPort = 18083) class DocumentNumberNotificationAuthentication { @@ -504,31 +757,101 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18083"); } - @Disabled("Request body has changed") @Test - void initNotificationAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json", "responses/notification-session-response.json"); + void initNotificationAuthentication_onlyRequeriedFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); assertNotNull(response); } + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), DOCUMENT_NUMBER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-0000-0000-000000000000", + "DEMO", + null, + null, + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(authenticationRequest, DOCUMENT_NUMBER)); + } + @Test void initNotificationAuthentication_userAccountNotFound_throwException() { - assertThrows(UserAccountNotFoundException.class, () -> { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); - }); + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); } - @Disabled("Request body has changed") @Test - void initNotificationAuthentication_requestIsUnauthorized_throwException() { - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request.json"); - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(), DOCUMENT_NUMBER); - }); + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); } } @@ -713,7 +1036,7 @@ void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException } @Test - void initLinkedNotificationSignature_documentNotFound_throwException() { + void initLinkedNotificationSignature_accountNotFound_throwException() { SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); assertThrows(UserAccountNotFoundException.class, @@ -721,7 +1044,7 @@ void initLinkedNotificationSignature_documentNotFound_throwException() { } @Test - void initLinkedNotificationSignature_accountNotFound_throwException() { + void initLinkedNotificationSignature_suitableAccountNotFound_throwException() { SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, @@ -729,7 +1052,17 @@ void initLinkedNotificationSignature_accountNotFound_throwException() { } @Test - void initLinkedNotificationSignature_ApiClientIsOutdated_throwException() { + void initLinkedNotificationSignature_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + var linkedSignatureSessionRequest = toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, "cmFuZG9tTm9uY2U=", new RequestProperties(true)); + connector.initLinkedNotificationSignature(linkedSignatureSessionRequest, DOCUMENT_NUMBER); + }); + } + + @Test + void initLinkedNotificationSignature_apiClientBeingUsedIsOutdated_throwException() { SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); assertThrows(SmartIdClientException.class, @@ -1195,41 +1528,45 @@ void initNotificationSignature_throwsServerMaintenanceException() { } } - private static AuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { + private DeviceLinkAuthenticationSessionRequest toQrAuthenticationSessionRequest() { + return toDeviceLinkAuthenticationSessionRequest(null, null); + } + + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest(RequestProperties requestProperties, + String initialCallbackUrl) { var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( Base64.toBase64String("a".repeat(32).getBytes()), "rsassa-pss", new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new AuthenticationSessionRequest( + return new DeviceLinkAuthenticationSessionRequest( "00000000-0000-0000-0000-000000000000", "DEMO", - "QUALIFIED", + CertificateLevel.QUALIFIED.name(), SignatureProtocol.ACSP_V2, signatureProtocolParameters, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Log in?"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + requestProperties, null, - null, - null + initialCallbackUrl ); } - private static AuthenticationSessionRequest toNotificationAuthenticationSessionRequest() { + private static NotificationAuthenticationSessionRequest toNotificationAuthenticationSessionRequest(CertificateLevel certificateLevel, RequestProperties requestProperties) { var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( Base64.toBase64String("a".repeat(32).getBytes()), "rsassa-pss", - new SignatureAlgorithmParameters("SHA-512")); + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new AuthenticationSessionRequest( + return new NotificationAuthenticationSessionRequest( "00000000-0000-0000-0000-000000000000", "DEMO", - "QUALIFIED", - SignatureProtocol.ACSP_V2, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2.name(), signatureProtocolParameters, - InteractionUtil.encodeInteractionsAsBase64(List.of(NotificationInteraction.verificationCodeChoice("Verify the code"))), - null, + InteractionUtil.encodeToBase64(List.of(new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Login?"))), + requestProperties, null, - null + "numeric4" ); } @@ -1249,7 +1586,7 @@ private static SignatureSessionRequest createSignatureSessionRequest() { protocolParameters, null, null, - InteractionUtil.encodeInteractionsAsBase64(List.of(DeviceLinkInteraction.displayTextAndPIN("Sign the document"))), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign the document", null))), null, null); } @@ -1258,7 +1595,7 @@ private static SignatureSessionRequest createNotificationSignatureSessionRequest var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", "rsassa-pss", new SignatureAlgorithmParameters("SHA-512")); - var interaction = DeviceLinkInteraction.displayTextAndPIN("Verify the code"); + var interaction = new Interaction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Verify the code", null); return new SignatureSessionRequest("00000000-0000-0000-0000-000000000000", "DEMO", null, @@ -1266,7 +1603,7 @@ private static SignatureSessionRequest createNotificationSignatureSessionRequest protocolParameters, null, null, - InteractionUtil.encodeInteractionsAsBase64(List.of(interaction)), + InteractionUtil.encodeToBase64(List.of(interaction)), null, null ); diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json new file mode 100644 index 00000000..cabe10ef --- /dev/null +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json @@ -0,0 +1,7 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +} \ No newline at end of file diff --git a/src/test/resources/requests/device-link-authentication-session-request.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json similarity index 100% rename from src/test/resources/requests/device-link-authentication-session-request.json rename to src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json index 4af72eb4..ecfae2f5 100644 --- a/src/test/resources/requests/device-link-authentication-session-request.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json @@ -2,6 +2,7 @@ "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", "signatureProtocolParameters": { "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", "signatureAlgorithm": "rsassa-pss", @@ -9,6 +10,5 @@ "hashAlgorithm": "SHA3-512" } }, - "certificateLevel": "QUALIFIED", "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json new file mode 100644 index 00000000..46af4e84 --- /dev/null +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json @@ -0,0 +1,18 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "initialCallbackUrl": "https://example.com/callback" +} \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json new file mode 100644 index 00000000..dbd2f3b5 --- /dev/null +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json @@ -0,0 +1,15 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "initialCallbackUrl": "https://example.com/callback" +} \ No newline at end of file diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json new file mode 100644 index 00000000..3a9c0349 --- /dev/null +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json @@ -0,0 +1,18 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "vcType": "numeric4" +} diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json new file mode 100644 index 00000000..0f88439e --- /dev/null +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json @@ -0,0 +1,4 @@ +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO" +} diff --git a/src/test/resources/requests/notification-authentication-session-request.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json similarity index 50% rename from src/test/resources/requests/notification-authentication-session-request.json rename to src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json index c65465ed..ab3d1b61 100644 --- a/src/test/resources/requests/notification-authentication-session-request.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json @@ -4,12 +4,11 @@ "signatureProtocol": "ACSP_V2", "signatureProtocolParameters": { "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss" - }, - "interactions": [ - { - "type": "verificationCodeChoice", - "displayText60": "Verify the code" + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" } - ] + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "vcType": "numeric4" } \ No newline at end of file diff --git a/src/test/resources/responses/device-link-authentication-session-response.json b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json similarity index 100% rename from src/test/resources/responses/device-link-authentication-session-response.json rename to src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json diff --git a/src/test/resources/responses/auth/notification/notification-session-response.json b/src/test/resources/responses/auth/notification/notification-session-response.json new file mode 100644 index 00000000..6b21efb5 --- /dev/null +++ b/src/test/resources/responses/auth/notification/notification-session-response.json @@ -0,0 +1,4 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file diff --git a/src/test/resources/responses/notification-session-response.json b/src/test/resources/responses/notification-session-response.json deleted file mode 100644 index 90e37fa3..00000000 --- a/src/test/resources/responses/notification-session-response.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "vc": { - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future", - "type": "alphaNumeric4", - "value": "4927" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" -} \ No newline at end of file From 8f10793e7391231e6ca9ddb3332478345d736525 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 19 Sep 2025 16:42:23 +0300 Subject: [PATCH 44/57] Update notification-based authentication session status validations (#137) * SLIB-117 - add path for creating linked signature session with request and response objects * SLIB-117 - reduce duplicated code in SmartIdRestConnector * SLIB-109 - add notification-based authentication request object and update builder * SLIB-109 - change RpChallengeGenerator to return RpChallenge object instead of string * SLIB-109 - change interactions for device-link and notification-based flows * SLIB-110 - add initial notification-based auth sessions status validations * SLIB-110 - add initial notification-based auth sessions status validations * SLIB-110 - update DeviceLinkBuilder validation exception messages and additional validations * SLIB-110 - improve interactions validations * SLIB-110 - add javadocs; add tests; refactor non-qualified certificate purpose validation and authentication purpose validations * SLIB-110 - update Readme; improve javadocs --- MIGRATION_GUIDE.md | 2 +- README.md | 76 +++++-- .../ee/sk/smartid/AuthenticationIdentity.java | 150 ++++++------- .../ee/sk/smartid/AuthenticationResponse.java | 4 +- ...eLinkAuthenticationResponseValidator.java} | 68 ++---- ...nkAuthenticationSessionRequestBuilder.java | 10 +- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 61 +++--- ...iceLinkSignatureSessionRequestBuilder.java | 4 +- .../ee/sk/smartid/ErrorResultHandler.java | 2 +- ...dSignatureCertificatePurposeValidator.java | 24 +-- ...cationAuthenticationResponseValidator.java | 184 ++++++++++++++++ ...onAuthenticationSessionRequestBuilder.java | 23 +- ...icationSignatureSessionRequestBuilder.java | 5 +- src/main/java/ee/sk/smartid/RpChallenge.java | 4 +- .../SignatureCertificatePurposeValidator.java | 4 + ...enticationCertificatePurposeValidator.java | 46 ++++ ...ionCertificatePurposeValidatorFactory.java | 43 ++++ ...ertificatePurposeValidatorFactoryImpl.java | 50 +++++ ...enticationCertificatePurposeValidator.java | 55 +++++ ...enticationCertificatePurposeValidator.java | 77 +++++++ .../sk/smartid/common/InteractionsMapper.java | 7 +- ...nQualifiedSmartIdCertificateValidator.java | 72 +++++++ ...tIdAuthenticationCertificateValidator.java | 113 ++++++++++ .../rest/dao/NotificationInteraction.java | 25 +++ .../ee/sk/smartid/util/InteractionUtil.java | 14 +- ...kAuthenticationResponseValidatorTest.java} | 26 +-- ...thenticationSessionRequestBuilderTest.java | 11 +- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 199 ++++++++++++----- ...inkSignatureSessionRequestBuilderTest.java | 11 +- ...natureCertificatePurposeValidatorTest.java | 4 +- ...onAuthenticationResponseValidatorTest.java | 204 ++++++++++++++++++ ...thenticationSessionRequestBuilderTest.java | 47 +++- .../ee/sk/smartid/QrCodeGeneratorTest.java | 1 + .../SignatureResponseValidatorTest.java | 2 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 25 ++- ...cationCertificatePurposeValidatorTest.java | 97 +++++++++ ...cationCertificatePurposeValidatorTest.java | 84 ++++++++ .../integration/ReadmeIntegrationTest.java | 45 ++-- .../auth-cert-40504040001-demo-q.crt | 38 ++++ .../test-certs/nq-auth-cert-40504049999.crt | 38 ++++ 40 files changed, 1646 insertions(+), 309 deletions(-) rename src/main/java/ee/sk/smartid/{AuthenticationResponseValidator.java => DeviceLinkAuthenticationResponseValidator.java} (74%) create mode 100644 src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java create mode 100644 src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java create mode 100644 src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java create mode 100644 src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java create mode 100644 src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java create mode 100644 src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java rename src/test/java/ee/sk/smartid/{AuthenticationResponseValidatorTest.java => DeviceLinkAuthenticationResponseValidatorTest.java} (85%) create mode 100644 src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java create mode 100644 src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java create mode 100644 src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt create mode 100644 src/test/resources/test-certs/nq-auth-cert-40504049999.crt diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d4b8231c..67bf42f2 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,7 +26,7 @@ It is recommended to start using device-link authentication flows from Smart-ID 1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` 2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` 3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. - - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. Link and QR-code should be recreated after every second. + - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. 4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. 5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. 6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. diff --git a/README.md b/README.md index bc13a3d3..b4991eef 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ This library supports Smart-ID API v3.1. * [Device link authentication session](#device-link-authentication-session) * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) - * [Initiating a dynamic-link authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) - * [Initiating a dynamic-link authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) + * [Initiating a device link-based authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) + * [Initiating a device link-based authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) * [Device-link signature session](#device-link-signature-session) * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) @@ -48,6 +48,8 @@ This library supports Smart-ID API v3.1. * [Validating sessions status response](#validating-session-status-response) * [Setting up CertificateValidator](#set-up-certificatevalidator) * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) + * [Example of validating device link-based authentication session status](#device-link-based-authentication-session-status-validation) + * [Example of validating notification-based authentication session status](#notification-based-authentication-session-status-validation) * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) * [Example of validating the signature](#example-of-validating-the-signature-session-response) * [Error handling for session status](#error-handling-for-session-status) @@ -59,7 +61,7 @@ This library supports Smart-ID API v3.1. * [Linked notification-based signature session](#linked-notification-based-signature-session) * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) * [Notification-based flows](#notification-based-flows) - * [Differences between notification-based and device link flows](#differences-between-notification-based-and-device-link-flows) + * [Differences between notification-based, device link-based flows and linked flows](#differences-between-notification-based-device-link-based-and-linked-flows) * [Notification-based authentication session](#notification-based-authentication-session) * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) @@ -127,7 +129,7 @@ import ee.sk.smartid.SmartIdConnector; ## Test accounts for testing -[Test accounts for testing](https://github.com/SK-EID/smart-id-documentation/wiki/Environment-technical-parameters#test-accounts-for-automated-testing) +[Test accounts for testing](https://sk-eid.github.io/smart-id-documentation/test_accounts.html) ## Logging @@ -142,13 +144,16 @@ logging.level.ee.sk.smartid.rest.LoggingFilter: trace ## Setting up SmartIdClient for v3.1 +[Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) +NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO + ```java InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load(is, "changeit".toCharArray()); var smartIdClient = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); client.setTrustStore(trustStore); @@ -631,7 +636,7 @@ String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "pn ## Session status request handling for v3.1 The Smart-ID v3.1 API includes new session status request path for retrieving session results. -Session status request is to be used for dynamic-link and notification-based flows. +Session status request is to be used for device link-based and notification-based flows. ### Session status response @@ -672,7 +677,7 @@ if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ #### Example of querying sessions status only once The following example shows how to use the SessionStatusPoller to only query the sessions status single time. -NB! If using this method for dynamic-link flows. Make sure the pollingSleepTimeout is not set or does not impact generating the dynamic-content for every second. +NB! If using this method for device link-based flows. Make sure the pollingSleepTimeout is not set or does not impact generating the QR-code for every second. ```java *SessionResponse sessionResponse; @@ -733,18 +738,20 @@ CertificateValidator certificateValidator = new CertificateValidatorImpl(trusted #### Example of validating the authentication sessions response: -AuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) +##### Device link-based authentication session status validation + +DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) ```java // Set up AuthenticationResponseValidator with the CertificateValidator -AuthenticationResponseValidator authenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); +DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); // Create authentication request builder DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; // Initialize session DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); // Get request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); +DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); // get sessions result SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); @@ -753,7 +760,33 @@ SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.ses // validate sessions state is completed if("COMPLETE".equals(sessionStatus.getState())){ // validate the session status response with authentication session request and return authentication identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` + +##### Notification-based authentication session status validation + +NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +// Set up AuthenticationResponseValidator with the CertificateValidator +NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +NotificationAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); + +// get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); } ``` @@ -931,6 +964,7 @@ Second part of the linked signature flow. Will be used to start the signature se * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. * `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. @@ -973,17 +1007,21 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ## Notification-based flows -### Differences between notification-based and device link flows +### Differences between notification-based, device link-based and linked flows * `Notification-Based flow` * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. * Known users or devices: - * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using dynamic-link flows. + * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using device link-based flows. * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. * `Device Link flow` * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. - * Authentication and certificate-choice support unknown users or devices: Useful when the user's identity or device is not known in advance. + * Anonymous authentication: the user's details are not required beforehand. RP validates the user after the Smart-ID authentication is completed. * Real-time updates: QR-code needs to be refreshed every second to ensure validity. +* `Linked flow` + * Combination of anonymous certificate choice and notification-based signing: Starts with a device link-based certificate choice session followed by a notification-based signing session. + * QR-code or device link will be used only for the certificate choice part of the flow. + * Supports only device link-based interactions in the signature part of the flow. ### Notification-based authentication session @@ -1018,7 +1056,7 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- String documentNumber = "PNOLT-40504040001-MOCK-Q"; // For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RandomChallenge.generate(); +RpChallenge rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -1050,7 +1088,7 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( ); // For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RandomChallenge.generate(); +RpChallenge rpChallenge = RpChallengeGenerator.generate(); // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response @@ -1074,6 +1112,9 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ### Notification-based certificate choice session +> [!CAUTION] +> The notification-based certificate choice has not yet been updated to be used with Smart-ID API v3.1 + #### Request parameters * `relyingPartyUUID`: Required. UUID of the Relying Party. @@ -1112,6 +1153,9 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ### Notification-based signature session +> [!CAUTION] +> The notification-based signature has not yet been updated to be used with Smart-ID API v3.1 + #### Request Parameters The request parameters for the notification-based signature session are as follows: diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index dd85ab92..d0847509 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,77 +30,81 @@ import java.time.LocalDate; import java.util.Optional; +/** + * Interpretation of users identity in the validate authentication certificate + */ public class AuthenticationIdentity { - private String givenName; - private String surname; - private String identityNumber; - private String country; - private X509Certificate authCertificate; - private LocalDate dateOfBirth; - - public AuthenticationIdentity() { - } - - public AuthenticationIdentity(X509Certificate authCertificate) { - this.authCertificate = authCertificate; - } - - public String getGivenName() { - return givenName; - } - - public void setGivenName(String givenName) { - this.givenName = givenName; - } - - public String getSurname() { - return surname; - } - - public void setSurname(String surname) { - this.surname = surname; - } - - public String getIdentityNumber() { - return identityNumber; - } - - public void setIdentityNumber(String identityNumber) { - this.identityNumber = identityNumber; - } - - public String getIdentityCode() { - return identityNumber; - } - - public void setIdentityCode(String identityCode) { - this.identityNumber = identityCode; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public X509Certificate getAuthCertificate() { - return authCertificate; - } - - /** - * Person's date of birth. - * NB! This information is not available for some Latvian certificates. - * - * @return Date of birth if this information is available in authentication response or empty optional. - */ - public Optional getDateOfBirth() { - return Optional.ofNullable(dateOfBirth); - } - - public void setDateOfBirth(LocalDate dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } + + private String givenName; + private String surname; + private String identityNumber; + private String country; + private X509Certificate authCertificate; + private LocalDate dateOfBirth; + + public AuthenticationIdentity() { + } + + public AuthenticationIdentity(X509Certificate authCertificate) { + this.authCertificate = authCertificate; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + + public String getIdentityNumber() { + return identityNumber; + } + + public void setIdentityNumber(String identityNumber) { + this.identityNumber = identityNumber; + } + + public String getIdentityCode() { + return identityNumber; + } + + public void setIdentityCode(String identityCode) { + this.identityNumber = identityCode; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public X509Certificate getAuthCertificate() { + return authCertificate; + } + + /** + * Person's date of birth. + * NB! This information is not available for some Latvian certificates. + * + * @return Date of birth if this information is available in authentication response or empty optional. + */ + public Optional getDateOfBirth() { + return Optional.ofNullable(dateOfBirth); + } + + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } } diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index 9d8f4013..2e7b4183 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -34,8 +34,8 @@ /** * Represents the authentication response after a successful authentication sessions status response was received. - * - *

    Use with {@link AuthenticationResponseValidator} to validate the certificate and the signature. + *

    + * Use with {@link DeviceLinkAuthenticationResponseValidator} to validate the auth certificate and signature. */ public class AuthenticationResponse { diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java similarity index 74% rename from src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java rename to src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index cefc35c5..5c2f513d 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -26,18 +26,12 @@ * #L% */ -import static org.slf4j.LoggerFactory.getLogger; - import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; import java.util.Base64; -import java.util.List; -import java.util.Set; - -import org.slf4j.Logger; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; @@ -47,46 +41,43 @@ /** * Validates authentication response and converts it to {@link AuthenticationIdentity} */ -public class AuthenticationResponseValidator { - - private static final Logger logger = getLogger(AuthenticationResponseValidator.class); - - private static final Set ALLOWED_AUTHENTICATION_EXTENDED_KEY_USAGE = Set.of("1.3.6.1.5.5.7.3.2", "1.3.6.1.4.1.62306.5.7.0"); - private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; - private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; - private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; +public class DeviceLinkAuthenticationResponseValidator { private final CertificateValidator certificateValidator; private final SignatureValueValidator signatureValueValidator; private final AuthenticationResponseMapper authenticationResponseMapper; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; /** - * Creates an instance of {@link AuthenticationResponseValidator} + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} * * @param certificateValidator validator used to verify the authentication certificate is valid and trusted * @param authenticationResponseMapper the mapper to convert session status to authentication response * @param signatureValueValidator validator used to verify the correctness of the authentication signature value */ - public AuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator) { + public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidator) { this.certificateValidator = certificateValidator; this.authenticationResponseMapper = authenticationResponseMapper; this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidator; } /** - * Creates an instance of {@link AuthenticationResponseValidator} using {@link CertificateValidator} + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} using {@link CertificateValidator} * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} * * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @return a new instance of {@link AuthenticationResponseValidator} + * @return a new instance of {@link DeviceLinkAuthenticationResponseValidator} */ - public static AuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { - return new AuthenticationResponseValidator(certificateValidator, + public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new DeviceLinkAuthenticationResponseValidator(certificateValidator, new AuthenticationResponseMapperImpl(), - new SignatureValueValidatorImpl()); + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); } /** @@ -126,28 +117,9 @@ public AuthenticationIdentity validate(SessionStatus sessionStatus, private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { validateCertificateLevel(authenticationResponse, requestedCertificateLevel); certificateValidator.validate(authenticationResponse.getCertificate()); - validateCertificatePurpose(authenticationResponse); - } - - private void validateCertificatePurpose(AuthenticationResponse authenticationResponse) { - X509Certificate certificate = authenticationResponse.getCertificate(); - try { - List extendedKeyUsage = certificate.getExtendedKeyUsage(); - if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(ALLOWED_AUTHENTICATION_EXTENDED_KEY_USAGE::contains)) { - logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); - throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); - } - - boolean[] keyUsage = certificate.getKeyUsage(); - if (keyUsage == null - || !(keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] - || keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { - logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); - throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); - } - } catch (CertificateParsingException ex) { - throw new UnprocessableSmartIdResponseException("Authentication certificate is incorrect", ex); - } + AuthenticationCertificatePurposeValidator purposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + purposeValidator.validate(authenticationResponse.getCertificate()); } private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index 5e7f084e..a7451f7e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -47,7 +47,7 @@ import ee.sk.smartid.util.StringUtil; /** - * Class for building a device link authentication session request + * Builder for creating a device-link authentication session */ public class DeviceLinkAuthenticationSessionRequestBuilder { @@ -258,7 +258,7 @@ public DeviceLinkSessionResponse initAuthenticationSession() { */ public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Authentication session request has not been initialized yet"); + throw new SmartIdClientException("Device link authentication session has not been initialized yet"); } return authenticationSessionRequest; } @@ -309,13 +309,9 @@ private void validateSignatureParameters() { } private void validateInteractions() { - if (interactions == null || interactions.isEmpty()) { + if (InteractionUtil.isEmpty(interactions)) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } - validateNoDuplicateInteractions(); - } - - private void validateNoDuplicateInteractions() { if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 4e51fc4e..a9a3f26b 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -28,6 +28,8 @@ import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.crypto.Mac; @@ -38,7 +40,7 @@ import jakarta.ws.rs.core.UriBuilder; /** - * Builds Smart-ID device link URI. + * Builder for creating Smart-ID device-link URI. */ public class DeviceLinkBuilder { @@ -230,7 +232,7 @@ public URI createUnprotectedUri() { UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); addElapsedSecondsIfQrCode(uriBuilder); uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) - .queryParam("version", version).queryParam("lang", lang); + .queryParam("version", version).queryParam("lang", lang); return uriBuilder.build(); } @@ -251,14 +253,17 @@ public URI buildDeviceLink(String sessionSecret) { private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { if (elapsedSeconds != null) { if (deviceLinkType != DeviceLinkType.QR_CODE) { - throw new SmartIdClientException("elapsedSeconds is only valid for QR_CODE deviceLinkType"); + throw new SmartIdClientException("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE"); } uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); } } private String generateAuthCode(String unprotectedLink, String sessionSecret) { - validateAuthCodeParams(unprotectedLink); + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter 'sessionSecret' cannot be empty"); + } + validateAuthCodeParams(); return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); } @@ -289,60 +294,68 @@ private String calculateAuthCode(String data, String base64Key) { mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); - } catch (Exception e) { - throw new SmartIdClientException("Failed to calculate authCode", e); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException ex) { + throw new SmartIdClientException("Failed to calculate authCode", ex); } } private void validateInputParameters() { if (StringUtil.isEmpty(deviceLinkBase)) { - throw new SmartIdClientException("Parameter deviceLinkBase must be set"); + throw new SmartIdClientException("Parameter 'deviceLinkBase' cannot be empty"); } if (StringUtil.isEmpty(version)) { - throw new SmartIdClientException("Parameter version must be set"); + throw new SmartIdClientException("Parameter 'version' cannot be empty"); } if (!ALLOWED_VERSION.equals(version)) { throw new SmartIdClientException("Only version 1.0 is allowed"); } if (deviceLinkType == null) { - throw new SmartIdClientException("Parameter deviceLinkType must be set"); + throw new SmartIdClientException("Parameter 'deviceLinkType' must be set"); } if (sessionType == null) { - throw new SmartIdClientException("Parameter sessionType must be set"); + throw new SmartIdClientException("Parameter 'sessionType' must be set"); } if (StringUtil.isEmpty(sessionToken)) { - throw new SmartIdClientException("Parameter sessionToken must be set"); + throw new SmartIdClientException("Parameter 'sessionToken' cannot be empty"); } if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { - throw new SmartIdClientException("elapsedSeconds must be set for QR_CODE deviceLinkType"); + throw new SmartIdClientException("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE"); } if (StringUtil.isEmpty(lang)) { - throw new SmartIdClientException("Parameter lang must be set"); + throw new SmartIdClientException("Parameter 'lang' must be set"); } } - private void validateAuthCodeParams(String unprotectedLink) { + private void validateAuthCodeParams() { if (StringUtil.isEmpty(schemeName)) { - throw new SmartIdClientException("Parameter schemeName must be set"); + throw new SmartIdClientException("Parameter 'schemeName' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyNameBase64)) { - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdClientException("Parameter 'relyingPartyName' cannot be empty"); } boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { - throw new SmartIdClientException("initialCallbackUrl must be empty for QR_CODE flow"); + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE"); } if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { - throw new SmartIdClientException("initialCallbackUrl must be provided for same-device flows"); + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP"); } - - if (sessionType != SessionType.CERTIFICATE_CHOICE && StringUtil.isEmpty(digest)) { - throw new SmartIdClientException("digest must be set for AUTH or SIGN flows"); + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } + if (StringUtil.isEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } } - // TODO - 07.09.25: add interactions validation when only for certificate choice case should not be provided, otherwise required, fix in SLIB-110 - if (StringUtil.isEmpty(unprotectedLink)) { - throw new SmartIdClientException("unprotected device-link must not be empty"); + if (sessionType == SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isNotEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } + if (StringUtil.isNotEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } } } } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 9f6e7e94..789ca28f 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -46,7 +46,7 @@ import ee.sk.smartid.util.StringUtil; /** - * Builder class for creating device link signature session + * Builder for creating a device-link signature session */ public class DeviceLinkSignatureSessionRequestBuilder { @@ -321,7 +321,7 @@ private void validateRequestParameters() { } private void validateInteractions() { - if (interactions == null || interactions.isEmpty()) { + if (InteractionUtil.isEmpty(interactions)) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index 155cb8fd..e628775b 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -57,7 +57,7 @@ public class ErrorResultHandler { * @throws SmartIdClientException when input parameter sessionResult is null * @throws UserActionException sub-exceptions based on end result * @throws UserAccountException sub-exceptions based on end result - * @throws ProtocolFailureException when there was a error in the process (e.g shcema name incorrect) + * @throws ProtocolFailureException when there was an error in the process (e.g. shcema name is incorrect) * @throws ExpectedLinkedSessionException when different session type was started than expected * @throws SmartIdServerException when technical error occurred on server side * @throws UnprocessableSmartIdResponseException when unexpected end result was received diff --git a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java index 84b674c4..7498e7b0 100644 --- a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java @@ -27,11 +27,8 @@ */ import java.security.cert.X509Certificate; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import ee.sk.smartid.common.certifiate.NonQualifiedSmartIdCertificateValidator; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.util.CertificateAttributeUtil; @@ -45,29 +42,12 @@ */ public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { - private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSignatureCertificatePurposeValidator.class); - - private static final Set NONQUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); - @Override public void validate(X509Certificate certificate) { - validateCertificateHasNonQualifiedSmartIdCertificatePolicies(certificate); + NonQualifiedSmartIdCertificateValidator.validate(certificate); validateCertificateCanBeUsedForSigning(certificate); } - private static void validateCertificateHasNonQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); - } - if (!certificatePolicyOids.containsAll(NONQUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Non-qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", NONQUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate does not contain required non-qualified certificate policy OIDs"); - } - } - private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java new file mode 100644 index 00000000..4f726c2d --- /dev/null +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -0,0 +1,184 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates notification-based authentication session status + */ +public class NotificationAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + */ + public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + } + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @return a new instance of {@link NotificationAuthenticationResponseValidator} + */ + public static NotificationAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new NotificationAuthenticationResponseValidator(certificateValidator, + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name + * @return the authentication identity + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name + * @param brokeredRpName the brokered relying party name + * @return the authentication identity + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel())); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + + private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (authenticationResponse.getCertificateLevel() == null) { + throw new SmartIdClientException("Certificate level is not provided"); + } + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + // TODO - 18.09.25: everything except constructing the payload is same with device link authentication response validator, should refato + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + Base64.getEncoder().encodeToString(authenticationSessionRequest.relyingPartyName().getBytes(StandardCharsets.UTF_8)), + StringUtil.isEmpty(brokeredRpName) ? "" : Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)), + Base64.getEncoder().encodeToString(calculateInteractionsDigest(authenticationSessionRequest)), + authenticationResponse.getInteractionTypeUsed(), + "", + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); + } + + private static void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private static byte[] calculateInteractionsDigest(NotificationAuthenticationSessionRequest authenticationSessionRequest) { + byte[] interactions = authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8); + return DigestCalculator.calculateDigest(interactions, HashAlgorithm.SHA_256); + } +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index ea3d2721..b694ffb5 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -33,6 +33,7 @@ import ee.sk.smartid.common.InteractionsMapper; import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; @@ -46,7 +47,7 @@ import ee.sk.smartid.util.StringUtil; /** - * Class for building a notification-based authentication session request + * Builder for creating a notification-based authentication session */ public class NotificationAuthenticationSessionRequestBuilder { @@ -64,6 +65,8 @@ public class NotificationAuthenticationSessionRequestBuilder { private SemanticsIdentifier semanticsIdentifier; private String documentNumber; + private NotificationAuthenticationSessionRequest notificationAuthenticationSessionRequest; + /** * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector * @@ -218,14 +221,26 @@ public NotificationAuthenticationSessionResponse initAuthenticationSession() { NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); validateResponseParameters(notificationAuthenticationSessionResponse); + this.notificationAuthenticationSessionRequest = authenticationRequest; return notificationAuthenticationSessionResponse; } + /** + * Returns the built authentication session request + * + * @return the built authentication session request + */ + public NotificationAuthenticationSessionRequest getAuthenticationSessionRequest() { + if (notificationAuthenticationSessionRequest == null) { + throw new SmartIdClientException("Notification-based authentication session has not been initialized yet"); + } + return notificationAuthenticationSessionRequest; + } + private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { if (semanticsIdentifier != null && documentNumber != null) { throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } else - if (semanticsIdentifier != null) { + } else if (semanticsIdentifier != null) { return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); } else if (documentNumber != null) { return connector.initNotificationAuthentication(authenticationRequest, documentNumber); @@ -266,7 +281,7 @@ private void validateSignatureParameters() { } private void validateInteractions() { - if (interactions == null || interactions.isEmpty()) { + if (InteractionUtil.isEmpty(interactions)) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 8dc64f42..65ab044a 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -48,6 +48,9 @@ import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.StringUtil; +/** + * Builder for creating a notification-based signature session + */ public class NotificationSignatureSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); @@ -277,7 +280,7 @@ private void validateParameters() { } private void validateAllowedInteractions() { - if (interactions == null || interactions.isEmpty()) { + if (InteractionUtil.isEmpty(interactions)) { throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); } if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { diff --git a/src/main/java/ee/sk/smartid/RpChallenge.java b/src/main/java/ee/sk/smartid/RpChallenge.java index 568a6f57..eefc7ac7 100644 --- a/src/main/java/ee/sk/smartid/RpChallenge.java +++ b/src/main/java/ee/sk/smartid/RpChallenge.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java index 34b7174c..3c91ed2d 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java @@ -30,6 +30,10 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +/** + * Interface for validating whether a given X509 certificate is suitable for digital signing purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for signing. + */ public interface SignatureCertificatePurposeValidator { /** diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java new file mode 100644 index 00000000..4e48ff03 --- /dev/null +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java @@ -0,0 +1,46 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating whether a given X509 certificate is suitable for authentication purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for authentication. + */ +public interface AuthenticationCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for authentication + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for authentication + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java new file mode 100644 index 00000000..a35d5b3f --- /dev/null +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java @@ -0,0 +1,43 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory interface for creating {@link AuthenticationCertificatePurposeValidator} instances + */ +public interface AuthenticationCertificatePurposeValidatorFactory { + + /** + * Creates an {@link AuthenticationCertificatePurposeValidator} for the specified certificate level. + * + * @param certificateLevel the level of the authentication certificate + * @return an instance of {@link AuthenticationCertificatePurposeValidator} suitable for the given level + */ + AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java new file mode 100644 index 00000000..6e23507e --- /dev/null +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java @@ -0,0 +1,50 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory implementation for creating {@link AuthenticationCertificatePurposeValidator} + * instances based on the provided {@link AuthenticationCertificateLevel}. + *

    + * Returns a validator suitable for the certificate level: + *

      + *
    • {@code QUALIFIED} - returns {@link QualifiedAuthenticationCertificatePurposeValidator}
    • + *
    • {@code ADVANCED} - returns {@link NonQualifiedAuthenticationCertificatePurposeValidator}
    • + *
    + */ +public class AuthenticationCertificatePurposeValidatorFactoryImpl implements AuthenticationCertificatePurposeValidatorFactory { + + @Override + public AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel) { + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedAuthenticationCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedAuthenticationCertificatePurposeValidator(); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java new file mode 100644 index 00000000..dcd1e414 --- /dev/null +++ b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java @@ -0,0 +1,55 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.common.certifiate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.common.certifiate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Validator for non-qualified Smart-ID authentication certificates. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + *

    + * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class NonQualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + NonQualifiedSmartIdCertificateValidator.validate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } +} diff --git a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java new file mode 100644 index 00000000..ca62af1a --- /dev/null +++ b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java @@ -0,0 +1,77 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.common.certifiate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the authentication certificate is a qualified Smart-ID certificate and can be used for authentication. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Authentication + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (authentication) for Qualified profile + *

    + * * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class QualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + private final Logger logger = LoggerFactory.getLogger(QualifiedAuthenticationCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.2042.1.2"); + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + validateCertificateIsQualifiedSmartIdCertificate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } + + private void validateCertificateIsQualifiedSmartIdCertificate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a qualified Smart-ID authentication certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java index 97a9e8b7..acc37671 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,6 +27,7 @@ */ import java.util.List; +import java.util.Objects; import ee.sk.smartid.rest.dao.Interaction; @@ -55,6 +56,6 @@ public static Interaction from(T interaction) { * @return list of interactions to be used in REST request */ public static List from(List interactions) { - return interactions.stream().map(InteractionsMapper::from).toList(); + return interactions.stream().filter(Objects::nonNull).map(InteractionsMapper::from).toList(); } } diff --git a/src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java new file mode 100644 index 00000000..2e5f85a8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java @@ -0,0 +1,72 @@ +package ee.sk.smartid.common.certifiate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validator for non-qualified Smart-ID certificates. Can be used for both authentication and signing certificates. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) and (authentification) for Non-Qualified profile + */ +public final class NonQualifiedSmartIdCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSmartIdCertificateValidator.class); + + private static final Set NON_QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); + + private NonQualifiedSmartIdCertificateValidator() { + } + + /** + * Validates that the provided certificate is a non-qualified Smart-ID certificate. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is not a non-qualified Smart-ID certificate + */ + public static void validate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate"); + } + if (!certificatePolicyOids.containsAll(NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a non-qualified Smart-ID certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java new file mode 100644 index 00000000..93c44f04 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java @@ -0,0 +1,113 @@ +package ee.sk.smartid.common.certifiate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Validator for Smart-ID authentication certificates. + *

    + * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified and Non-Qualified and Digital authentication + */ +public final class SmartIdAuthenticationCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(SmartIdAuthenticationCertificateValidator.class); + + private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; + private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; + private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; + + private SmartIdAuthenticationCertificateValidator() { + } + + /** + * Validates that the provided certificate can be used for authentication. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate cannot be used for authentication + */ + public static void validate(X509Certificate certificate) { + if (!(isAfterApril2025Certificates(certificate) || isBeforeApril2025Certificates(certificate))) { + throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); + } + } + + // From April 2025 forward + // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 + // KeyUsage - digitalSignature + private static boolean isAfterApril2025Certificates(X509Certificate certificate) { + if (hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + // Before April 2025 + // Extended key usage - 1.3.6.1.5.5.7.3.2 + // Key Usage - digitalSignature, keyEncipherment, dataEncipherment + private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { + if (hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null + && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] + && keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] + && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + private static boolean hasExtendedKey(X509Certificate certificate, String oid) { + try { + List extendedKeyUsage = certificate.getExtendedKeyUsage(); + if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { + logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); + return true; + } + } catch (CertificateParsingException ex) { + throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); + } + return false; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java new file mode 100644 index 00000000..b5fe3eaf --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java @@ -0,0 +1,25 @@ +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ diff --git a/src/main/java/ee/sk/smartid/util/InteractionUtil.java b/src/main/java/ee/sk/smartid/util/InteractionUtil.java index 270f2b9c..aa9ac2a3 100644 --- a/src/main/java/ee/sk/smartid/util/InteractionUtil.java +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -29,14 +29,16 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; +import java.util.Objects; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.common.SmartIdInteraction; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.Interaction; /** - * Utility class for interactions related actions + * Utility for interactions related actions */ public class InteractionUtil { @@ -60,4 +62,14 @@ public static String encodeToBase64(List interactions) { throw new SmartIdClientException("Unable to encode interactions to Base64", ex); } } + + /** + * Checks if the list of interactions is empty or contains only null values + * + * @param interactions list of interactions + * @return true if the list is empty or contains only null values, false otherwise + */ + public static boolean isEmpty(List interactions) { + return interactions == null || interactions.stream().filter(Objects::nonNull).toList().isEmpty(); + } } diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java similarity index 85% rename from src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java rename to src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java index 53bf2cf9..e22d369d 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java @@ -59,19 +59,19 @@ import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; import ee.sk.smartid.util.InteractionUtil; -class AuthenticationResponseValidatorTest { +class DeviceLinkAuthenticationResponseValidatorTest { private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - private AuthenticationResponseValidator authenticationResponseValidator; + private DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator; @BeforeEach void setUp() { TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - authenticationResponseValidator = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); } @Disabled("Can make this work when TEST numbers will be available in the DEMO env") @@ -80,7 +80,7 @@ void validate() { String rpChallenge = ""; SessionStatus sessionStatus = new SessionStatus(); DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -98,7 +98,7 @@ void validate_certificateLevelHigherThanRequested_ok() { sessionStatus.setCert(cert); var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -112,27 +112,27 @@ class ValidateInputs { @Test void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); } @Test void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); } } @Test void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> authenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); } @@ -143,7 +143,7 @@ class ValidateSessionStatusCertificate { void validate_certificateLevelLowerThanRequested_throwException() { var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", ""); - var ex = assertThrows(CertificateLevelMismatchException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); } @@ -152,9 +152,9 @@ void validate_certificateLevelLowerThanRequested_throwException() { void validate_certificateCannotBeUsedForAuthentication_throwException() { var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", ""); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); } } @@ -166,7 +166,7 @@ class ValidateAuthenticationSignature { void validate_invalidSignature_throwException() { var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); assertEquals("Signature value validation failed", ex.getMessage()); } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 2286a5f7..e5e007a2 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -289,6 +289,15 @@ void initAuthenticationSession_interactionsIsEmpty_throwException(List b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + @ParameterizedTest @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { @@ -399,7 +408,7 @@ void getAuthenticationSessionRequest_authenticationNotInitialized_throwsExceptio DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); - assertEquals("Authentication session request has not been initialized yet", ex.getMessage()); + assertEquals("Device link authentication session has not been initialized yet", ex.getMessage()); } private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index 0d25f510..cdd8f4ec 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -117,7 +117,7 @@ void createUri_missingDeviceLinkBase_throwsException(String base) { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter deviceLinkBase must be set", ex.getMessage()); + assertEquals("Parameter 'deviceLinkBase' cannot be empty", ex.getMessage()); } @ParameterizedTest @@ -136,7 +136,7 @@ void createUri_missingVersion_throwsException(String version) { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter version must be set", ex.getMessage()); + assertEquals("Parameter 'version' cannot be empty", ex.getMessage()); } @Test @@ -153,7 +153,7 @@ void createUri_missingDeviceLinkType_throwsException() { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter deviceLinkType must be set", ex.getMessage()); + assertEquals("Parameter 'deviceLinkType' must be set", ex.getMessage()); } @Test @@ -169,7 +169,7 @@ void createUri_missingSessionType_throwsException() { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter sessionType must be set", ex.getMessage()); + assertEquals("Parameter 'sessionType' must be set", ex.getMessage()); } @ParameterizedTest @@ -187,7 +187,7 @@ void createUri_missingSessionToken_throwsException(String token) { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter sessionToken must be set", ex.getMessage()); + assertEquals("Parameter 'sessionToken' cannot be empty", ex.getMessage()); } @Test @@ -203,7 +203,7 @@ void createUri_missingElapsedSecondsForQrCode_throwsException() { .withLang(LANGUAGE) .createUnprotectedUri() ); - assertEquals("elapsedSeconds must be set for QR_CODE deviceLinkType", ex.getMessage()); + assertEquals("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE", ex.getMessage()); } @ParameterizedTest @@ -221,7 +221,7 @@ void createUri_missingLang_throwsException(String lang) { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("Parameter lang must be set", ex.getMessage()); + assertEquals("Parameter 'lang' must be set", ex.getMessage()); } @Test @@ -238,7 +238,7 @@ void createUri_elapsedSecondsSetForNonQrCode_throwsException() { .withElapsedSeconds(ELAPSED_SECONDS) .createUnprotectedUri() ); - assertEquals("elapsedSeconds is only valid for QR_CODE deviceLinkType", ex.getMessage()); + assertEquals("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE", ex.getMessage()); } } @@ -254,13 +254,13 @@ void buildDeviceLink(SessionType sessionType) { .withSessionType(sessionType) .withDeviceLinkType(DeviceLinkType.QR_CODE) .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) // // TODO - 07.09.25: fix this, if certificate choice then it should be empty .withLang(LANGUAGE) .withElapsedSeconds(1L) .withRelyingPartyName(RELYING_PARTY_NAME); if (sessionType != SessionType.CERTIFICATE_CHOICE) { - builder.withDigest(BASE64_DIGEST); + builder.withDigest(BASE64_DIGEST) + .withInteractions(BASE64_INTERACTIONS); } URI uri = builder.buildDeviceLink(SESSION_SECRET); @@ -282,12 +282,32 @@ void buildDeviceLink_withCustomSchemeName() { .withElapsedSeconds(1L) .withDigest(BASE64_DIGEST) .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(BASE64_INTERACTIONS) .buildDeviceLink(SESSION_SECRET) ).get("authCode"); assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); } + @Test + void buildDeviceLink_sameDeviceFlowWithCallback_ok() { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withInitialCallbackUrl(CALLBACK_URL) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + @ParameterizedTest @NullAndEmptySource void buildDeviceLink_missingSchemeName_throwsException(String scheme) { @@ -304,7 +324,7 @@ void buildDeviceLink_missingSchemeName_throwsException(String scheme) { .withRelyingPartyName(RELYING_PARTY_NAME) .buildDeviceLink(SESSION_SECRET) ); - assertEquals("Parameter schemeName must be set", ex.getMessage()); + assertEquals("Parameter 'schemeName' cannot be empty", ex.getMessage()); } @Test @@ -322,17 +342,39 @@ void buildDeviceLink_missingRelyingPartyName_throwsException() { .withDigest(BASE64_DIGEST) .buildDeviceLink(SESSION_SECRET) ); - assertEquals("Parameter relyingPartyName must be set", ex.getMessage()); + assertEquals("Parameter 'relyingPartyName' cannot be empty", ex.getMessage()); } - @Test - void buildDeviceLink_missingDigestForAuth_throwsException() { + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForAuthentication_throwsException(String digest) { var ex = assertThrows(SmartIdClientException.class, () -> new DeviceLinkBuilder() .withDeviceLinkBase(DEVICE_LINK_BASE) .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForSignature_throwsException(String digest) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) .withBrokeredRpName(BROKERED_RP) .withInteractions(BASE64_INTERACTIONS) .withLang(LANGUAGE) @@ -340,7 +382,26 @@ void buildDeviceLink_missingDigestForAuth_throwsException() { .withRelyingPartyName(RELYING_PARTY_NAME) .buildDeviceLink(SESSION_SECRET) ); - assertEquals("digest must be set for AUTH or SIGN flows", ex.getMessage()); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @Test + void buildDeviceLink_certificateChoiceAndDigestIsSet_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDigest(BASE64_DIGEST) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); } @Test @@ -360,7 +421,7 @@ void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { .withInitialCallbackUrl(CALLBACK_URL) .buildDeviceLink(SESSION_SECRET) ); - assertEquals("initialCallbackUrl must be empty for QR_CODE flow", exception.getMessage()); + assertEquals("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE", exception.getMessage()); } @ParameterizedTest @@ -379,55 +440,90 @@ void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLi .withRelyingPartyName(RELYING_PARTY_NAME) .buildDeviceLink(SESSION_SECRET) ); - assertEquals("initialCallbackUrl must be provided for same-device flows", exception.getMessage()); + assertEquals("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP", exception.getMessage()); } - @Test - void buildDeviceLink_withEmptyUnprotectedUri_shouldThrowException() { - var builder = new DeviceLinkBuilder() { - @Override - public URI createUnprotectedUri() { - return URI.create(""); - } - }; - - builder - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withElapsedSeconds(1L) - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withLang(LANGUAGE) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME); + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForAuthentication_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(SESSION_SECRET)); - assertEquals("unprotected device-link must not be empty", exception.getMessage()); + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForSignature_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); } @Test - void buildDeviceLink_sameDeviceFlowWithCallback() { - URI uri = new DeviceLinkBuilder() + void buildDeviceLink_interactionsSetForCertificateChoice_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); + } + + @Test + void buildDeviceLink_invalidBase64Key_shouldThrowException() { + var builder = new DeviceLinkBuilder() .withDeviceLinkBase(DEVICE_LINK_BASE) .withSessionToken(SESSION_TOKEN) .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withDeviceLinkType(DeviceLinkType.QR_CODE) .withBrokeredRpName(BROKERED_RP) .withInteractions(BASE64_INTERACTIONS) .withLang(LANGUAGE) - .withInitialCallbackUrl(CALLBACK_URL) + .withElapsedSeconds(1L) .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET); + .withRelyingPartyName(RELYING_PARTY_NAME); - Map params = toQueryParamsMap(uri); - assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); + + assertEquals("Failed to calculate authCode", exception.getMessage()); + assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); } - @Test - void calculateAuthCode_invalidBase64Key_shouldThrowException() { + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_sessionSecretIsEmpty_throwException(String sessionSecret) { var builder = new DeviceLinkBuilder() .withDeviceLinkBase(DEVICE_LINK_BASE) .withSessionToken(SESSION_TOKEN) @@ -440,10 +536,9 @@ void calculateAuthCode_invalidBase64Key_shouldThrowException() { .withDigest(BASE64_DIGEST) .withRelyingPartyName(RELYING_PARTY_NAME); - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(sessionSecret)); - assertEquals("Failed to calculate authCode", exception.getMessage()); - assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); + assertEquals("Parameter 'sessionSecret' cannot be empty", exception.getMessage()); } } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 3bd6f586..8f156e53 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -38,6 +38,7 @@ import java.net.URI; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.UnaryOperator; @@ -355,13 +356,21 @@ void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) @ParameterizedTest @NullAndEmptySource - void initSignatureSession_whenInteractionsIsNullOrEmpty(List interactions) { + void initSignatureSession_whenInteractionsIsNullOrEmpty_throwException(List interactions) { var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); } + @Test + void initSignatureSession_interactionsListWithNullValue_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + @ParameterizedTest @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { diff --git a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java index e7cd8392..a303dbfc 100644 --- a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java @@ -76,7 +76,7 @@ void validate_certificatePoliciesAreMissing_throwException() { X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); } @Test @@ -90,7 +90,7 @@ void validate_invalidCertificatePolicies_throwException() { X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, null, null); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not contain required non-qualified certificate policy OIDs", ex.getMessage()); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); } @ParameterizedTest diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java new file mode 100644 index 00000000..8ae7b76f --- /dev/null +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java @@ -0,0 +1,204 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; + +class NotificationAuthenticationResponseValidatorTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + private static final String SIGNATURE_VALUE = "DR6pERkYxg5+pa7c0675yEmithtHzEnsqMyOD7RgZlwJgyR/z7VBxOZOUxdakjkT2LK6Jfo3RqxMeYciGfidieJ6vbdyzLDoSnrreaJguo1W5n5blz6Zqb+bkum/30qex7S31ubmRnNM/yIIVJ+/uuAgZgoQIwUV/KmTOE+0GEWFbqvxqFr7BkfrMX4luRrzfXkzpiWqlO8DXoQq4zfo3c00JsvCiM7PSfK4TLVR1FldXmGiV4ftcIep+YoPIxzzIbToyZ0+XYLIgobBio3EHyp2Z3rEWjASfY7+27c0TLkx8gRchxUcowxepioS49lz0trhMzbxNe1NCskHUAa3oodIH0xPNVD/B03uEKziK0r8mGWanHFvOhlqxnCfeN3AuQi5BJ0X7oybMWEvJ06dHlRBc3LrKhM1RrKkSiMy/eI0lTXDajJPupp7Zq/Ck41GbFnn52woFwYAB0hP2kUf7patya9C5C4QyeWB7SnRqtWTXprOMlPHG/KAjh7d61BhjV94zrFKj6YHcDxoQ6a31laYuyhkPMhqdzui1E/4BhWNiJsMkiqdB++VEgL5eT/76xHQuHIUD4GXHmAJnsQjBjFx5ws/yl5pFWsc/GR5H5oNT73Iaw2WSPReXLr7ZD8XEWmTV/GhjXoRUoEjtJrEIv30dYjXqE9Kv+B89tVk2gPHutgNuJJwwoZUaP61ym9w3WawR7ElJ3A8lvYjBPPOY3nYK/hu10imk/9cjdBJaNnMAlfsyzaXtBwBqdu5d80ibFAXkQ9aLwkqURX/Xnmw+lXIzj+p4T2BzhaGR7994qCVksoWPP/0xdvO+lYDM0YLPTvZTXN2PZVgt9NqYTEZHG6/4bcGoIkDTutAxF859rHBplzlMOGDz+sZPKHnLrKMnWaSaSbCVHi7pwF2vcq6QxkzY0grRAKYmmObPP7ORhIjXt5ENoW6n5CptgowizS4CckiaAe0u3QtMp+NoGYg/LSeef7NFhDDf8tUK0azHlAUDb3HPGUtQ3dvYX3JlCoX"; + + private NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + notificationAuthenticationResponseValidator = NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + } + + @Test + void validate_ok() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", SIGNATURE_VALUE); + + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); + } + } + + @Test + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); + } + + @Nested + class ValidateSessionStatusCertificate { + + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", SIGNATURE_VALUE); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", SIGNATURE_VALUE); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + } + + @Nested + class ValidateAuthenticationSignature { + + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signature value validation failed", ex.getMessage()); + } + } + + private static NotificationAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new NotificationAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2.name(), + new AcspV2SignatureProtocolParameters("3mhDkd0ulDR/WVZx678FcrNw4pUhrZxcQsmejf8jQ1HtSp3GAxCH/Fi9EEiuULp44G/KNKONPXZELqCSZw4AoA==", + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB3aXRoIFNtYXJ0LUlEIGRlbW8/In1d", + null, + null, + "numeric4"); + } + + private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-40504040001-DEMO-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom("9eZeWMTJ9YYBtjj5jK8p1sLm"); + signature.setUserChallenge("RvrVNS1GJYCsuEnEqPCdHHn5vl65F3XiBjmxB4zSosw"); + signature.setValue(signatureValue); + signature.setFlowType(FlowType.NOTIFICATION.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index 6ebd8297..a84e2991 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -83,12 +83,7 @@ void initAuthenticationSession_withDocumentNumber_ok() { verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); + assertAuthenticationSessionRequest(request); } @Test @@ -272,6 +267,15 @@ void initAuthenticationSession_interactionsAreEmpty_throwException(List b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + @ParameterizedTest @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { @@ -318,6 +322,26 @@ void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException } } + @Test + void getAuthenticationSessionRequest_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + builder.initAuthenticationSession(); + NotificationAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Notification-based authentication session has not been initialized yet", ex.getMessage()); + } + private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); } @@ -339,6 +363,15 @@ private static String generateBase64String(String text) { return Base64.toBase64String(text.getBytes()); } + private static void assertAuthenticationSessionRequest(NotificationAuthenticationSessionRequest request) { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + } + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index 4a8f1e18..f0c4560c 100644 --- a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -162,6 +162,7 @@ private static URI createUri() { .withElapsedSeconds(1L) .withRelyingPartyName("DEMO") .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions("interactions") .withLang("ENG"); return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index d93356cc..5f7d52d2 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -55,7 +55,7 @@ class SignatureResponseValidatorTest { private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - // // TODO - 31.08.25: replace these values when the test accounts are available + // TODO - 31.08.25: replace these values when the test accounts are available private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 56988a37..5e599891 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -57,6 +57,7 @@ import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureSessionRequest; class SmartIdClientTest { @@ -611,12 +612,13 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { var signableHash = new SignableHash("a".repeat(32).getBytes()); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initSignatureSession(); + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + SignatureSessionRequest request = builder.getSignatureSessionRequest(); URI deviceLink = smartIdClient.createDynamicContent() .withSchemeName("smart-id-demo") @@ -626,6 +628,7 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { .withSessionToken(response.sessionToken()) .withLang("eng") .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) .withInitialCallbackUrl(INITIAL_CALLBACK_URL) .buildDeviceLink(response.sessionSecret()); @@ -640,11 +643,12 @@ void createDynamicContent_withQrCode() { var signableHash = new SignableHash("a".repeat(32).getBytes()); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + SignatureSessionRequest request = builder.getSignatureSessionRequest(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); @@ -657,6 +661,7 @@ void createDynamicContent_withQrCode() { .withSessionToken(response.sessionToken()) .withLang("eng") .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) .buildDeviceLink(response.sessionSecret()); assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); @@ -670,11 +675,12 @@ void createDynamicContent_withQrCodeImage() { var signableHash = new SignableHash("a".repeat(32).getBytes()); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() .withDocumentNumber(DOCUMENT_NUMBER) .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + SignatureSessionRequest request = builder.getSignatureSessionRequest(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); URI qrCodeUri = smartIdClient.createDynamicContent() @@ -686,6 +692,7 @@ void createDynamicContent_withQrCodeImage() { .withSessionToken(response.sessionToken()) .withLang("eng") .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) .buildDeviceLink(response.sessionSecret()); String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); diff --git a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java new file mode 100644 index 00000000..c3521c02 --- /dev/null +++ b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -0,0 +1,97 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class NonQualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); + private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); + + private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new NonQualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + purposeValidator.validate(NQ_AUTH_CERT); + } + + @Test + void validate_certificateNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java new file mode 100644 index 00000000..2cd85766 --- /dev/null +++ b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -0,0 +1,84 @@ +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.*; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class QualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt")); + + private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new QualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.createCertificate(null, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, null, null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + // TODO - 19.09.25: validate missing KeyUsage and invalid KeyUsage scenarios +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 09cc67ab..cdfd8767 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -52,18 +52,20 @@ import ee.sk.smartid.AuthenticationCertificateLevel; import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationResponseValidator; import ee.sk.smartid.CertificateByDocumentNumberResult; import ee.sk.smartid.CertificateChoiceResponse; import ee.sk.smartid.CertificateChoiceResponseValidator; import ee.sk.smartid.CertificateLevel; import ee.sk.smartid.CertificateValidator; import ee.sk.smartid.CertificateValidatorImpl; +import ee.sk.smartid.DeviceLinkAuthenticationResponseValidator; import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; import ee.sk.smartid.DeviceLinkType; import ee.sk.smartid.FileTrustedCAStoreBuilder; import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.NotificationAuthenticationResponseValidator; +import ee.sk.smartid.NotificationAuthenticationSessionRequestBuilder; import ee.sk.smartid.QrCodeGenerator; import ee.sk.smartid.RpChallenge; import ee.sk.smartid.RpChallengeGenerator; @@ -83,6 +85,7 @@ import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; @@ -175,9 +178,9 @@ void anonymousAuthentication_withApp2App() { // Set up AuthenticationResponseValidator TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationResponseValidator authenticationResponseValidator = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = authenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -255,7 +258,7 @@ void authentication_withSemanticIdentifierAndQrCode() { // Validate the response and return user's identity TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); @@ -322,7 +325,7 @@ void authentication_withDocumentNumberAndQrCode() { // Validate the certificate and signature, then map the authentication response to the user's identity TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); @@ -537,14 +540,18 @@ void authentication_withDocumentNumber() { // Generate verification code to be displayed to the user String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient .createNotificationAuthentication() .withDocumentNumber(documentNumber) .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); + NotificationInteraction.displayTextAndPin("Log in?"))); + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // SessionID is used to query sessions status later String sessionId = authenticationSessionResponse.sessionID(); @@ -561,8 +568,9 @@ void authentication_withDocumentNumber() { // validate the sessions status and return user's identity TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: authentication request will be fixed with notification-based authentication changes, fix in SLIB-110 + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -587,14 +595,18 @@ void authentication_withSemanticIdentifier() { // Generate verification code to be displayed to the user String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - NotificationAuthenticationSessionResponse authenticationSessionResponse = smartIdClient - .createNotificationAuthentication() + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient.createNotificationAuthentication() .withSemanticsIdentifier(semanticIdentifier) .withRpChallenge(rpChallenge.toBase64EncodedValue()) .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); + NotificationInteraction.displayTextAndPin("Log in?"))); + + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); // SessionID is used to query sessions status later String sessionId = authenticationSessionResponse.sessionID(); @@ -610,8 +622,9 @@ void authentication_withSemanticIdentifier() { TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = AuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, null, "smart-id-demo"); // TODO - 02.07.25: will be fixed with notification-based authentication changes, fix in SLIB-110 + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); diff --git a/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt new file mode 100644 index 00000000..74b2a841 --- /dev/null +++ b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGqDCCBi6gAwIBAgIQDG5nZZQTInaS9mOLZwUmLDAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjUwOTA4MTIyNTIyWhcNMjgwOTA3MTIyNTIxWjBX +MQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzENMAsGA1UEBAwEVEVTVDEL +MAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkq +hkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjZXgGleu94rffU4c2iMM6J0eunUhbISt +ps5unECMwjKRohRJkwJlWToHv99Qs90vFM6rxMmZVKS6MZJ3WjkgbE0dxVVsWLnA +oBZWaZIidAgmQ6l0Cj0bLKjrRzKU/8T4gqy8tVrOJCvvwAlUIawzUdkQ+WdBCu79 +ipm2Lhh0KYkc20a9jNPfFwCOTkuO1AcVkhN6kdjIZgLrtlngKuhqNyotQmJ4Tl7+ +HkAtKV1zojIAY/LusoB5WWivdp6ey7hW4CyFAUhWpRnILcGeQ7sRBswQthEERThA +d5GRuiRWiz6eGrMAo7niINwoDNWwJDCeeAiJtYsjUBVyMyjxbPhX6DtwwZ3gP3NO +iwBQJ3qGDPNu0zGVdbph8+x9NZvkzAcPi8kr67S377BiH8AZPThzzkbadZxgIJrF +9WsTneKzAAk37HfGs3ESpFYaj8jlrI9RGJldh/nhTEdanqLhbGPO0gLXPJXR7pDG ++X9p6lC+sAa6mLVvBE5YSjQJoKCq8fsOfEQ0kyLcNem7rkxLh1ybsk57aDmZPPmx +Su853re+WvqJR8zPgD1qs7/LSKWeOOyi56OcVQ7Dmp6c56JH5JKQdUGbEXtkH+b6 +hHSMTXB/q/1OBwM4Tf7qO+AYU6MDVDdiEC0yAJm85b6cjbaxmY8a8eR2E3QuhV0H +7XLig9ebhoVb8cPJQmJxLMB1UHM4klQcav4F7+aaIOmEx60L0c7IITHU524LVjsP +GyqstXZxugSYh9V0h+aMdhtAj/JhWIh50fYrcaiyXZBu/nyRvzkY/6m5w/ycdiJ4 +tTNn+R5sJzdpo6SHQ5FnKRnpRnS7vrVSOy/quuYvvE06dE4fZrJfxAsGvg1L+qNT +yhpXECq9AF8U4Mqp03fnI5Dxglhx5rmNJZQKFM7PEDul0dV3lyAf6fmDPj/5H4L7 +RQyh5RcPEKbyk3Aw6v6RT69VuxXO2MvWDQx+S96Hk4105+BzWMWbOYyCWDRJz7IP +WTMQW5q1VnZTKElR1kxj6Ae7pITrlDDnAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAA +MB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQw +YjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k +ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIw +MjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1E +RU1PLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0 +cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlv +bi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsG +AQUFBwkBMREYDzE5OTkwMTAxMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUH +ADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIw +MjRlLmNybDAdBgNVHQ4EFgQUwT4r2UCtHWBnpnI3SjeaAGQyGlkwDgYDVR0PAQH/ +BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMGDy0L8r0KQxdmz7+6ZpzAtnoB7t6wP5 +YURwjJu5ysBMuYMIbw/5+R1Xv4nPo7BdQgIxAO3QXB8kg9w1jt8br7Sw2NUyUQZi ++Gt7a5Km+pxsMkiqCQYndI1Jrg/pW1gfUBRT2Q== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/nq-auth-cert-40504049999.crt b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt new file mode 100644 index 00000000..dd4d987b --- /dev/null +++ b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGjjCCBhSgAwIBAgIQM1MMPZyR0fwWexCl4aAn6zAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI0MTAxNTE4NDMxOFoXDTI3MTAxNTE4NDMxN1ow +YzELMAkGA1UEBhMCTFQxFjAUBgNVBAMMDVRFU1ROVU1CRVIsT0sxEzARBgNVBAQM +ClRFU1ROVU1CRVIxCzAJBgNVBCoMAk9LMRowGAYDVQQFExFQTk9MVC00MDUwNDA0 +OTk5OTCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAcLdOmpoXwhh41xRM +MqkTl7EVlD7kFgQxX32yVKDGG1wIB/iEaIlc/JRmPwpPVV996gbaBsOmsTSkdhyo +QuqLiov/gQ8a5/ByKBgGxLflCgtV6SsVqzJZoPBhs0KiVKwTWgA2aLv+oaKcDbrv +40Tz5J0DrgevAQctYm6eM1plJ1n/J5sINFgdXHv1k6iqSCMKb35vLZiywHoXo11H +ERX6HcfoYgaBcuWTTv5c6XS4nbfl2JzEsXpzshifRLwU/gQvkHqWiUv3TwpaRVrP +/ClzJP+s8fwx4mGgW0J4tSQqt4HWLpZOEu4ksIKijbQc7wLN006cV1O8EkZZhXud +bJWS3JJtmiFMwMc6fWa07UuMYHho86jZkKbJkhgQFX4TNzSm3O3CABNaVvqElagp +4OMYBbbe0HPPlEWGyoApvxVHaWa2mwtKomjZw11BuqZf7HevAKhURbOV8ImMzxii +woTfsBSyHniDW/mOy5hfgtkrvpXsy0BQ6NjdMcPLGW1Zp5DgrzjxXQhpJDzBUrna +eBQjKhh51a/siuby6eblm73gBHVLzDRK1Im0l4als5Lw/xoc8exlhFJhfg7+9L9a +NfUQAJWnUJ0cM3fd9O/PqqWlbk/R/1RUuIIhlmPhiPO+s/CWPZqbOIiasP1AyQMQ +N5p2+KRIQ9/RmoGz+fEzk063wR62YUms1iU7GTqqjSsficdVAfsnOQhsVwUXfOJx +ob2UYOh/RzEB9Uo+Mhwu4Omgxgl8PIL3nj/Lj8KmbdvUeAB9JgZ2xc5gbPY4GL4e +ShcyuxaNM4T/4N08WarBExI5GwJgqgaYA/J3agrqe9tcZJjlBPFT1A/O5Lc3Ewn8 +xlSvWe3r695PLf73ADi/vhjhmznqp72AmMYfmaj/WecS0FSfRPQP7ovb1N5DjIEx +U6GNF1Lb/9Q/z56uMf2y0tU35F4y/zhnBdZ+eSa69DoKD49RHRjMTA+UJNM4DZrN +uBszXI30OmlvGYZdzETQU52CpR79E2svr1hMto2G2/TA4YzzAgMBAAGjggHPMIIB +yzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIG +CCsGAQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtTlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1v +LnNrLmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0xU +LTQwNTA0MDQ5OTk5LU1PQ0stTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBU +BggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJj +ZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATAW +BgNVHSUEDzANBgsrBgEEAYPmYgUHADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8v +Yy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5jcmwwHQYDVR0OBBYEFBetuv93k5ps +ZBo46kmtdw4TGe24MA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAwNoADBlAjEA +llFf3tx3m9UVEhCmg3MFBtkD7rCaK6MSym/DEhR7LfXXOIWEuVAC0eaX8T2DyXxw +AjB1WKZRYM6awmjUpt3CdRSbQ0DcZbpWxjYjuHM3zkt2XkDwvXwEcfJbnnetIDDT +tSE= +-----END CERTIFICATE----- From f15fb98ace95928febfe83d6791e560352e21789 Mon Sep 17 00:00:00 2001 From: jalukse Date: Tue, 23 Sep 2025 18:01:56 +0300 Subject: [PATCH 45/57] Update publish script --- .github/workflows/publish.yaml | 22 ++++++++++++++++------ .github/workflows/tests.yaml | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 099ff7fb..cf005051 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -37,17 +37,27 @@ jobs: echo "[INFO] Artifact name: $artifact" ./mvnw versions:set -DnewVersion="$version" ./mvnw package -DskipTests - gpg -ab pom.xml - cd target + rm -rf ee/sk/smartid/smart-id-java-client/$version + mkdir ee/sk/smartid/smart-id-java-client/$version + cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ + cp ../pom.xml ee/sk/smartid/smart-id-java-client/$version/$artifact.pom + cd ee/sk/smartid/smart-id-java-client/$version + gpg -ab $artifact.pom gpg -ab $artifact.jar gpg -ab $artifact-sources.jar gpg -ab $artifact-javadoc.jar - jar -cvf bundle.jar ../pom.xml ../pom.xml.asc $artifact.jar $artifact.jar.asc $artifact-javadoc.jar $artifact-javadoc.jar.asc $artifact-sources.jar $artifact-sources.jar.asc - CODE=$(curl -w "%{http_code}" -o curl_response.txt -s -ujorlina2 -u ${{ secrets.SONATYPEUN }}:${{ secrets.SONATYPEPW }} --request POST -F "file=@bundle.jar" "https://oss.sonatype.org/service/local/staging/bundle_upload") + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha256sum "$file" | cut -d " " -f 1 > "$file.sha256"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha1sum "$file" | cut -d " " -f 1 > "$file.sha1"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do md5sum "$file" | cut -d " " -f 1 > "$file.md5"; done' _ {} + + cd ../../../../../ + zip bundle.zip ee/sk/smartid/smart-id-java-client/$version/* + CODE=$(curl -w "%{http_code}" -o curl_response.txt -s --request POST --verbose --header 'Authorization: Bearer ${{ secrets.SONATYPETOKEN }}' --form bundle=@bundle.zip https://central.sonatype.com/api/v1/publisher/upload) echo "[INFO] ------------------------------------------------------------------------" - echo "[INFO] Upload to oss.sonatype.org ResponseCode: $CODE" + echo "[INFO] Upload to central.sonatype.com ResponseCode: $CODE" cat curl_response.txt - echo -e "\n[INFO] Login to oss.sonatype.org for releasing $artifact" + echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" echo "[INFO] ------------------------------------------------------------------------" [[ $CODE == 201 ]] && exit 0 || exit 1 \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6e4229f8..f0598fd2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -2,7 +2,7 @@ name: Run tests on: push: - branches: [ "master" ] + branches: [ "master", "v3.1" ] pull_request: branches: [ "master" ] From e718f2cf028a4a1ed4952cf480e8386998774584 Mon Sep 17 00:00:00 2001 From: jalukse Date: Thu, 25 Sep 2025 11:01:07 +0300 Subject: [PATCH 46/57] Update publish.yaml --- .github/workflows/publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cf005051..3e939adc 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -38,7 +38,7 @@ jobs: ./mvnw versions:set -DnewVersion="$version" ./mvnw package -DskipTests rm -rf ee/sk/smartid/smart-id-java-client/$version - mkdir ee/sk/smartid/smart-id-java-client/$version + mkdir -p ee/sk/smartid/smart-id-java-client/$version cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ @@ -60,4 +60,4 @@ jobs: echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" echo "[INFO] ------------------------------------------------------------------------" [[ $CODE == 201 ]] && exit 0 || exit 1 - \ No newline at end of file + From 59c7619b93b4b92c7ba28b6be528cf858189ba64 Mon Sep 17 00:00:00 2001 From: jalukse Date: Thu, 25 Sep 2025 11:21:29 +0300 Subject: [PATCH 47/57] Update publish.yaml --- .github/workflows/publish.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3e939adc..703134e3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -37,6 +37,7 @@ jobs: echo "[INFO] Artifact name: $artifact" ./mvnw versions:set -DnewVersion="$version" ./mvnw package -DskipTests + cd target rm -rf ee/sk/smartid/smart-id-java-client/$version mkdir -p ee/sk/smartid/smart-id-java-client/$version cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ From de455b3202ed2447bce0a256180029b6f4950811 Mon Sep 17 00:00:00 2001 From: jalukse Date: Thu, 25 Sep 2025 13:20:53 +0300 Subject: [PATCH 48/57] Update check.yaml --- .github/workflows/check.yaml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 12371835..e1a57cbd 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -26,7 +26,7 @@ jobs: - name: Run dependency check run: | - ./mvnw org.owasp:dependency-check-maven:check + ./mvnw -DossIndexUsername=${{ secrets.ossIndexUsername }} -DossIndexPassword=${{ secrets.ossIndexPassword }} -DnvdApiKey=${{ secrets.nvdApiKey }} org.owasp:dependency-check-maven:check - name: Archive dependency report uses: actions/upload-artifact@v4 diff --git a/pom.xml b/pom.xml index 4bfa6833..7e13153b 100644 --- a/pom.xml +++ b/pom.xml @@ -258,7 +258,7 @@ org.owasp dependency-check-maven - 12.1.1 + 12.1.6 true false From dd0fef0a6c3c3fc462c73304be974a5f5e91a36f Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Thu, 25 Sep 2025 13:45:57 +0300 Subject: [PATCH 49/57] Improve authentication response validation (#138) * SLIB-110 - refactor common logic from DeviceLinkAuthenticationResponseValidator and NotificationAuthenticationResponseValidator to AuthenticationResponseValidator * SLIB-110 - improve certificate purpose validation tests for authentication certificate * SLIB-110 - improve documentation and code style * SLIB-110 - improve code style --- .../smartid/AuthenticationResponseMapper.java | 11 +- .../AuthenticationResponseMapperImpl.java | 2 +- .../AuthenticationResponseValidator.java | 169 ++++++++++++++++++ ...ceLinkAuthenticationResponseValidator.java | 116 ++---------- ...cationAuthenticationResponseValidator.java | 116 ++---------- src/main/java/ee/sk/smartid/TrailerField.java | 3 +- ...tIdAuthenticationCertificateValidator.java | 8 +- .../dao/AuthenticationSessionRequest.java | 36 ++++ ...eviceLinkAuthenticationSessionRequest.java | 2 +- ...ificationAuthenticationSessionRequest.java | 2 +- .../smartid/InvalidCertificateGenerator.java | 124 ++++++++----- ...natureCertificatePurposeValidatorTest.java | 6 +- ...thenticationSessionRequestBuilderTest.java | 2 +- ...natureCertificatePurposeValidatorTest.java | 19 +- ...cationCertificatePurposeValidatorTest.java | 74 +++++++- ...cationCertificatePurposeValidatorTest.java | 102 ++++++++++- .../util/CertificateAttributeUtilTest.java | 13 +- .../auth-pnolv-020100-29990-mock-q.crt | 46 +++++ 18 files changed, 566 insertions(+), 285 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java create mode 100644 src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index f2a31ee7..eb40e98c 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,6 +28,13 @@ import ee.sk.smartid.rest.dao.SessionStatus; +/** + * Represents a mapper for converting a SessionStatus to an AuthenticationResponse. + *

    + * Used to map the received session status to an authentication response object. + *

    + * Implementers should ensure that all mandatory fields are present. + */ public interface AuthenticationResponseMapper { /** diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 4edab95c..75639014 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -187,7 +187,7 @@ private static void validateSignature(SessionSignature sessionSignature) { private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - if (sessionSignature.getSignatureAlgorithmParameters() == null) { + if (signatureAlgorithmParameters == null) { throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); } if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java new file mode 100644 index 00000000..4c7d46d7 --- /dev/null +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java @@ -0,0 +1,169 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Represents a template to validate authentication session status response. + *

    + * Use implementations {@link DeviceLinkAuthenticationResponseValidator} or {@link NotificationAuthenticationResponseValidator} + * to validate the flow specific authentication response. + * + * @param the type of authentication session request + */ +abstract class AuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + private final SignatureValueValidator signatureValueValidator; + + protected AuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory, + SignatureValueValidator signatureValueValidator) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + this.signatureValueValidator = signatureValueValidator; + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name used in the QR-code or device link + * @return the authentication identity + */ + public final AuthenticationIdentity validate(SessionStatus sessionStatus, + T authenticationSessionRequest, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the authentication session status to be validated + * @param authenticationSessionRequest the authentication session request that was used to start the session + * @param schemaName the schema name used in the QR-code or device link + * @param brokeredRpName the brokered relying party name + * @return authentication identity containing details about the authenticated user + */ + public final AuthenticationIdentity validate(SessionStatus sessionStatus, + T authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + /** + * Constructs the payload used for signature validation. + * + * @param authenticationResponse the converted session status + * @param authenticationSessionRequest the authentication session request to start the session + * @param schemaName the schema name used in the QR-code or device link + * @param brokeredRpName the brokered relying party name + * @return the payload as a byte array + */ + protected abstract byte[] constructPayload(AuthenticationResponse authenticationResponse, + T authenticationSessionRequest, + String schemaName, + String brokeredRpName); + + /** + * Gets the requested certificate level from the authentication session request. + * + * @param authenticationSessionRequest the request to get certificate level from + * @return authentication certificate level + */ + protected abstract AuthenticationCertificateLevel getRequestedCertificateLevel(T authenticationSessionRequest); + + private void validateInputs(SessionStatus sessionStatus, T authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + T authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + protected static String toInteractionsBase64(String interactions) { + return Base64.getEncoder().encodeToString(calculateInteractionsDigest(interactions)); + } + + protected static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } + + private static byte[] calculateInteractionsDigest(String interactions) { + return DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index 5c2f513d..fa5c4247 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -27,26 +27,16 @@ */ import java.nio.charset.StandardCharsets; -import java.util.Base64; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.util.StringUtil; /** * Validates authentication response and converts it to {@link AuthenticationIdentity} */ -public class DeviceLinkAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; +public class DeviceLinkAuthenticationResponseValidator extends AuthenticationResponseValidator { /** * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} @@ -59,11 +49,8 @@ public class DeviceLinkAuthenticationResponseValidator { public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, AuthenticationResponseMapper authenticationResponseMapper, SignatureValueValidator signatureValueValidator, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidator) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidator; + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + super(certificateValidator, authenticationResponseMapper, authenticationCertificatePurposeValidatorFactory, signatureValueValidator); } /** @@ -80,81 +67,27 @@ public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertific new AuthenticationCertificatePurposeValidatorFactoryImpl()); } - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, schemaName, null); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name - * @param brokeredRpName the brokered relying party name - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateCertificate(authenticationResponse, AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel())); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + @Override + protected AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); } - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator purposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - purposeValidator.validate(authenticationResponse.getCertificate()); - } - - private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (authenticationResponse.getCertificateLevel() == null) { - throw new SmartIdClientException("Certificate level is not provided"); - } - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { + @Override + protected byte[] constructPayload(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { String[] payload = { schemaName, SignatureProtocol.ACSP_V2.name(), authenticationResponse.getServerRandom(), authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - Base64.getEncoder().encodeToString(authenticationSessionRequest.relyingPartyName().getBytes(StandardCharsets.UTF_8)), - StringUtil.isEmpty(brokeredRpName) ? "" : Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(calculateInteractionsDigest(authenticationSessionRequest)), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + toInteractionsBase64(authenticationSessionRequest.interactions()), authenticationResponse.getInteractionTypeUsed(), StringUtil.orEmpty(authenticationSessionRequest.initialCallbackUrl()), authenticationResponse.getFlowType().getDescription() @@ -163,21 +96,4 @@ private byte[] constructPayload(AuthenticationResponse authenticationResponse, .join("|", payload) .getBytes(StandardCharsets.UTF_8); } - - private static void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private static byte[] calculateInteractionsDigest(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { - byte[] interactions = authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8); - return DigestCalculator.calculateDigest(interactions, HashAlgorithm.SHA_256); - } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java index 4f726c2d..32d9e8dd 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -27,26 +27,16 @@ */ import java.nio.charset.StandardCharsets; -import java.util.Base64; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.util.StringUtil; /** * Validates notification-based authentication session status */ -public class NotificationAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; +public class NotificationAuthenticationResponseValidator extends AuthenticationResponseValidator { /** * Creates an instance of {@link NotificationAuthenticationResponseValidator} @@ -59,10 +49,7 @@ public class NotificationAuthenticationResponseValidator { public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, AuthenticationResponseMapper authenticationResponseMapper, SignatureValueValidator signatureValueValidator, AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + super(certificateValidator, authenticationResponseMapper, authenticationCertificatePurposeValidatorFactory, signatureValueValidator); } /** @@ -79,83 +66,27 @@ public static NotificationAuthenticationResponseValidator defaultSetupWithCertif new AuthenticationCertificatePurposeValidatorFactoryImpl()); } - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, schemaName, null); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name - * @param brokeredRpName the brokered relying party name - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateCertificate(authenticationResponse, AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel())); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - - private void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (authenticationResponse.getCertificateLevel() == null) { - throw new SmartIdClientException("Certificate level is not provided"); - } - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); + @Override + protected AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest.certificateLevel() == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); } - // TODO - 18.09.25: everything except constructing the payload is same with device link authentication response validator, should refato - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { + @Override + protected byte[] constructPayload(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { String[] payload = { schemaName, SignatureProtocol.ACSP_V2.name(), authenticationResponse.getServerRandom(), authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - Base64.getEncoder().encodeToString(authenticationSessionRequest.relyingPartyName().getBytes(StandardCharsets.UTF_8)), - StringUtil.isEmpty(brokeredRpName) ? "" : Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)), - Base64.getEncoder().encodeToString(calculateInteractionsDigest(authenticationSessionRequest)), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + toInteractionsBase64(authenticationSessionRequest.interactions()), authenticationResponse.getInteractionTypeUsed(), "", authenticationResponse.getFlowType().getDescription() @@ -164,21 +95,4 @@ private byte[] constructPayload(AuthenticationResponse authenticationResponse, .join("|", payload) .getBytes(StandardCharsets.UTF_8); } - - private static void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private static byte[] calculateInteractionsDigest(NotificationAuthenticationSessionRequest authenticationSessionRequest) { - byte[] interactions = authenticationSessionRequest.interactions().getBytes(StandardCharsets.UTF_8); - return DigestCalculator.calculateDigest(interactions, HashAlgorithm.SHA_256); - } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index 9966b2ec..91ce118f 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -29,7 +29,8 @@ import java.util.Arrays; /** - * TrailerField represents the value used in the trailer field of the Smart-ID authentication response and value necessary for generating the signature. + * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response. + * The pssSpecValue necessary for generating the signature. */ public enum TrailerField { diff --git a/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java index 93c44f04..f23ebf89 100644 --- a/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java @@ -69,7 +69,7 @@ public static void validate(X509Certificate certificate) { // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 // KeyUsage - digitalSignature private static boolean isAfterApril2025Certificates(X509Certificate certificate) { - if (hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { + if (!hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { return false; } boolean[] keyUsage = certificate.getKeyUsage(); @@ -84,7 +84,7 @@ private static boolean isAfterApril2025Certificates(X509Certificate certificate) // Extended key usage - 1.3.6.1.5.5.7.3.2 // Key Usage - digitalSignature, keyEncipherment, dataEncipherment private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { - if (hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { + if (!hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { return false; } boolean[] keyUsage = certificate.getKeyUsage(); @@ -103,11 +103,11 @@ private static boolean hasExtendedKey(X509Certificate certificate, String oid) { List extendedKeyUsage = certificate.getExtendedKeyUsage(); if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); - return true; + return false; } } catch (CertificateParsingException ex) { throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); } - return false; + return true; } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java new file mode 100644 index 00000000..1fb847a8 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java @@ -0,0 +1,36 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Marker interface for authentication session requests. + *

    + * Used to limit AuthenticationResponseValidator to + * only use authentication session request for validations. + */ +public interface AuthenticationSessionRequest { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java index abc502a4..a7d0b2be 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java @@ -53,5 +53,5 @@ public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, String interactions, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable, AuthenticationSessionRequest { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java index 316bf206..6376f375 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java @@ -52,5 +52,5 @@ public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, String interactions, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - String vcType) implements Serializable { + String vcType) implements Serializable, AuthenticationSessionRequest { } diff --git a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java index 668d099e..e3fc3a35 100644 --- a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java +++ b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java @@ -40,6 +40,7 @@ import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.PolicyInformation; @@ -53,60 +54,93 @@ public final class InvalidCertificateGenerator { private InvalidCertificateGenerator() { } - public static X509Certificate createCertificate(CertificatePolicies policies, - KeyUsage keyUsage, - QCStatement qcStatement) { - Security.addProvider(new BouncyCastleProvider()); - KeyPair kp = createKeyPair(); - X509V3CertificateGenerator certGen = getBaseX509Generator(kp); - if (policies != null) { - certGen.addExtension(Extension.certificatePolicies, false, policies); - } - if (keyUsage != null) { - certGen.addExtension(Extension.keyUsage, true, keyUsage); - } - if (qcStatement != null) { - certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); - } - return generate(certGen, kp); - } - public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { ASN1EncodableVector vec = new ASN1EncodableVector(); vec.addAll(policyInformations); return CertificatePolicies.getInstance(new DERSequence(vec)); } - private static KeyPair createKeyPair() { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); - kpg.initialize(2048); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new RuntimeException(e); - } + public static Builder builder() { + return new Builder(); } - private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { - X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); - X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); - - X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setIssuerDN(issuer); - certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); - certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); - certGen.setSubjectDN(subject); - certGen.setPublicKey(kp.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen; - } + public static class Builder { + + private CertificatePolicies policies; + private ExtendedKeyUsage extendedKeyUsage; + private KeyUsage keyUsage; + private QCStatement qcStatement; + + public Builder withPolicies(CertificatePolicies policies) { + this.policies = policies; + return this; + } + + public Builder withExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) { + this.extendedKeyUsage = extendedKeyUsage; + return this; + } + + public Builder withKeyUsage(KeyUsage keyUsage) { + this.keyUsage = keyUsage; + return this; + } + + public Builder withQcStatement(QCStatement qcStatement) { + this.qcStatement = qcStatement; + return this; + } + + public X509Certificate createCertificate() { + Security.addProvider(new BouncyCastleProvider()); + KeyPair kp = createKeyPair(); + X509V3CertificateGenerator certGen = getBaseX509Generator(kp); + if (policies != null) { + certGen.addExtension(Extension.certificatePolicies, false, policies); + } + if (extendedKeyUsage != null) { + certGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); + } + if (keyUsage != null) { + certGen.addExtension(Extension.keyUsage, true, keyUsage); + } + if (qcStatement != null) { + certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); + } + return generate(certGen, kp); + } + + private static KeyPair createKeyPair() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { + X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); + X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); + + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setIssuerDN(issuer); + certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); + certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); + certGen.setSubjectDN(subject); + certGen.setPublicKey(kp.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return certGen; + } - private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { - try { - return certGen.generateX509Certificate(kp.getPrivate(), "BC"); - } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { - throw new RuntimeException(e); + private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { + try { + return certGen.generateX509Certificate(kp.getPrivate(), "BC"); + } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { + throw new RuntimeException(e); + } } } } diff --git a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java index a303dbfc..26622f2c 100644 --- a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java @@ -73,7 +73,7 @@ void validate_ok() { @Test void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); @@ -87,7 +87,7 @@ void validate_invalidCertificatePolicies_throwException() { new DERSequence() ); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); @@ -105,7 +105,7 @@ void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) new DERSequence() ); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, keyUsage, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index a84e2991..dab26384 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -268,7 +268,7 @@ void initAuthenticationSession_interactionsAreEmpty_throwException(List b.withInteractions(Collections.singletonList(null))); diff --git a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java index 6c065c73..2069e9b9 100644 --- a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java @@ -71,7 +71,7 @@ void validate_ok() { @Test void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); @@ -85,7 +85,7 @@ void validate_invalidCertificatePolicies_throwException() { new DERSequence() ); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, null, null); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); @@ -101,7 +101,7 @@ void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) new ASN1ObjectIdentifier(QCP_N_QSCD_OID), new DERSequence()); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, keyUsage, null); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); @@ -116,14 +116,17 @@ void validate_QsStatementsExtensionIsMissing_throwException() { new ASN1ObjectIdentifier(QCP_N_QSCD_OID), new DERSequence()); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation), null); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); } @Test - void validate_QsStatementsDoesNotHaveElectronicSigning_throwException() { + void validate_qsStatementsDoesNotHaveElectronicSigning_throwException() { PolicyInformation skQPolicy = new PolicyInformation( new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), new DERSequence()); @@ -133,7 +136,11 @@ void validate_QsStatementsDoesNotHaveElectronicSigning_throwException() { CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); - X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation), qcStatement); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .withQcStatement(qcStatement) + .createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); diff --git a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java index c3521c02..7f26c056 100644 --- a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -34,6 +34,9 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.PolicyInformation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,6 +51,8 @@ class NonQualifiedAuthenticationCertificatePurposeValidatorTest { private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; @@ -69,7 +74,7 @@ void validate_certificateNotProvided_throwException() { @Test void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); @@ -83,15 +88,80 @@ void validate_invalidCertificatePolicies_throwException() { new DERSequence() ); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(policies, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); } + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + @Test void validate_certificateCannotBeUsedForAuthentication_throwException() { var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); } + + private static CertificatePolicies toNonQualifiedAuthCertificate() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } } diff --git a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java index 2cd85766..15aadb63 100644 --- a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,25 +26,35 @@ * #L% */ -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.security.cert.X509Certificate; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.PolicyInformation; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ee.sk.smartid.CertificateUtil; import ee.sk.smartid.FileUtil; import ee.sk.smartid.InvalidCertificateGenerator; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; class QualifiedAuthenticationCertificatePurposeValidatorTest { - private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt")); + private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt")); + private static final X509Certificate AUTH_CERT_BEFORE_APRIL_2025 = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-pnolv-020100-29990-mock-q.crt")); + private static final String SK_QUALIFIED_AUTH_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; + private static final String NCP_PLUS_POLICY_OID = "0.4.0.2042.1.2"; private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; @@ -54,13 +64,26 @@ void setUp() { } @Test - void validate_ok() { + void validate_authCert_afterApril2025_ok() { assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); } + // TODO - 23.09.25: Will leave it for now, as change might be needed for automated testing. + @Disabled("Test-certificate was created with 1.3.6.1.4.1.10015.3.17.2 and conflicts with required value 1.3.6.1.4.1.10015.17.2") + @Test + void validate_authCert_beforeApril2025_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT_BEFORE_APRIL_2025)); + } + + @Test + void validate_certificateIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + @Test void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); @@ -74,11 +97,74 @@ void validate_invalidCertificatePolicies_throwException() { new DERSequence() ); CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.createCertificate(policies, null, null); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); } - // TODO - 19.09.25: validate missing KeyUsage and invalid KeyUsage scenarios + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + private static CertificatePolicies toQualifiedSmartIdAuthPolicy() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_AUTH_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_PLUS_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } } diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index b4102378..3e742f4e 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -102,7 +102,7 @@ void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws Certificat @Test void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); @@ -120,14 +120,9 @@ void getCertificatePolicy_certificatePolicyPresent() throws CertificateException @Test void hasNonRepudiation_KeyUsageExtensionIsMissing() { - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); - - assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); - } - - @Test - void hasNonRepudiation_KeyUsageExtensionIsMising() { - X509Certificate certificate = InvalidCertificateGenerator.createCertificate(null, null, null); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withKeyUsage(null) + .createCertificate(); assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); } diff --git a/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt new file mode 100644 index 00000000..7df0ee18 --- /dev/null +++ b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIIDTCCBfWgAwIBAgIQM34W+9hlpYRjtEHrK9yczDANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMjMwMTAzMTQ1NTM5WhgPMjAzMDEyMTcyMzU5NTlaMGoxCzAJ +BgNVBAYTAkxWMRkwFwYDVQQDDBBURVNUTlVNQkVSLEFEVUxUMRMwEQYDVQQEDApU +RVNUTlVNQkVSMQ4wDAYDVQQqDAVBRFVMVDEbMBkGA1UEBRMSUE5PTFYtMDIwMTAw +LTI5OTkwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAp4yVDWinZexD +b/OUB7U414CuMbmzgo7ApXVML2sTU55XBbcQUCo15wkO9gP6u72YZ1TiEN1wbFxw +mBGGoVm5RRi/1WdQhSRcZ3TOPwE06VILayZ6kQo+D6WbqwazgWuCPYl858xe0E9k +PkEKeNhX3LgDVUFW9kq09HVPqJ2e4anw+eAF78bxjp2pN49IziZBsUjNjmcmartp +Qf5nFJEqIC3m+67oQoqkRtfTX7eKF5+pjoq6XzFgkwdp4Xnfq3wgN+fmqJF8tGeL +1jQxMqWWuqhwMUaklbW4s+M3vGVMQd5rDnBl5qRCPn+gNxseKAaueUchI2WGjOKU +QM7MSay0qeYd3s435cRwGub7asY6p/7Nn19ykQDPj9bV35eZ1GLL7Sb/QYXBUTcP +fd1lxjJwNYJIbpJ0Aj/qtgmGGbWa2ROs9v2l2jh8Re2YRaIvYVrVTiCHFOuMf+PH +O36qFOWdbGtZnYT3QEPQVe0DCXhsioMndEo/BSASGmgriqBaS+T6x9UcS6qbRkQ5 +m0piwaDrwaBSyP4oW60jbLA9SvWDancGOLXqOgEIyhdxuB3w0TFLytjZMq0olLjy +XeK6XL1A052CBu97/8GmZkTelg9qYMJmrVUaidby16bYUgjxSV9O+0w+ZzVDUuwE +p5LUQwoEBrylA2fD8gk9JhffsbbmrgvEOCk8f590KRt4JczND+WmmaXGTNv9Rbj2 +9pv0wqlpMV8iaPxxvdNydlE51K9baxopZibZdH3abiRhsyffDZckZHNIzExzE/V9 +6MYh9Myos8dDSR+HPg3F1i92PjsusJUMUyZFcs0cAWXV0Z/4G7yB4J1YeJx6GkbD +PLG8G1kvnh1mJAxmm/39MK3gHINIe1+JNc8c32Y75JDF4yjQftP0lwHx/WHwfvkq +nmLHYc9Sb2o/AYjbTsxOxqhnmkS/smn4R/t3/i4gzfvGnhc9Mh/5OduGeTWAP4CE +M6pgsCRlcGNwRNTzw3yW4nNZdFIsINru6Gtwy3PlQzuNjT9lvgt9AgMBAAGjggGt +MIIBqTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBcBgNVHSAEVTBTMEcGCisG +AQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1 +L2VuL3JlcG9zaXRvcnkvQ1BTLzAIBgYEAI96AQIwHQYDVR0OBBYEFBEOFq9GzHMr +rV6qgBoEw1Y9lEquMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MBMG +A1UdJQQMMAoGCCsGAQUFBwMCMHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYd +aHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6 +Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0 +MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMjAxMDAtMjk5OTAtTU9D +Sy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwMDAxMDIxMjAwMDBaMA0G +CSqGSIb3DQEBCwUAA4ICAQCwZnf+COHoIlkxIFPU33TCmh//k5oVDp42pK7Zp765 +KIppDRuxVtFHNvM5F4P9lGQ1FycPi+8N6uDX+XboKQ5SwtvcKYL23GfQwxnzMc0h +lyFm9Gx5Etl200CIP0hTCiFpEWouIOy3spGXwoaFjrcL+oYuL0HW7kFORSjerwOL +osTHJsT1geDY64INgO98i7WgHnmtMjoVXeyklVCsKwvYnMZVFzrpQkL7h9CQGffq +4S664jGYEghnZXh8uiq8x3l4V777NPuwCspiebXUrEmUe9lP/dHwaX009y4gygkB +VQSr7z8QTh3Cbt2g9Brt+w/PqKmYw+eJyQ21DbxPrQyZKQvFY+XsWkyWOFrRPsG+ +Rb7lqvejm8ppqQX7wH6ulvhkKT91vF1uy2icb77VB5i3m7LMSZF7BUa/U6Qlm2GK +Cz8+6FN3xiC+cMulWaMA7c0tiT9aWTqqPh5w9RfAIWXgbsG0vP+vSMtyRERcMpA+ +hjzB6Rj44j0Mg4PfKpvlsYG2aF5NXNRJpT2utm+Var44+HthQkltoWhGjXG9Rc7c +MQBRECohUqeNtV0GCQUnE2RtZvufidVw4sOx3qxmoU/dlribucQ4mVPZjVF3LVoX +IYJRUqtdMrh9dR9ZDAwuA1Y6sxr3Dw/QQujtU8NKAAGKIPsPVAir5Dfs7R+bF/dR +mg== +-----END CERTIFICATE----- From 22ea369331bfb44a894934ae1c1d876ab7d5c997 Mon Sep 17 00:00:00 2001 From: jalukse Date: Thu, 25 Sep 2025 14:18:57 +0300 Subject: [PATCH 50/57] Update check.yaml Able to run check manually --- .github/workflows/check.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index e1a57cbd..9e1f8b92 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,6 +5,7 @@ on: workflows: ["Run tests"] types: - completed + workflow_dispatch: permissions: contents: read @@ -36,4 +37,4 @@ jobs: - name: Run spotbugs check run: | - ./mvnw spotbugs:check \ No newline at end of file + ./mvnw spotbugs:check From 1226988f02de670bb4a4c1306b64583ad308da33 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 6 Oct 2025 16:01:34 +0300 Subject: [PATCH 51/57] Add additional tooling for same device flows (#139) * SLIB-126 - fix AuthenticationResponseValidator issue with authentication response started with initialCallbackUrl, but QR-code flow was used by user * SLIB-126 - add util to create callbackUrl with url-token * SLIB-126 - add util to create callbackUrl with url-token * SLIB-126 - improve comments * SLIB-126 - move UrlSafeTokenGenerator and CallbackUrl to common package * SLIB-126 - improve code style in CallbackUrlUtilTest --- CHANGELOG.md | 4 + README.md | 66 +++++++ .../AuthenticationResponseValidator.java | 169 ------------------ ...ceLinkAuthenticationResponseValidator.java | 138 ++++++++++++-- ...cationAuthenticationResponseValidator.java | 112 ++++++++++-- .../common/devicelink/CallbackUrl.java | 38 ++++ .../devicelink/UrlSafeTokenGenerator.java | 85 +++++++++ .../SessionSecretMismatchException.java} | 13 +- ...eviceLinkAuthenticationSessionRequest.java | 2 +- ...ificationAuthenticationSessionRequest.java | 2 +- .../ee/sk/smartid/util/CallbackUrlUtil.java | 91 ++++++++++ .../ee/sk/smartid/util/InteractionUtil.java | 13 ++ ...nkAuthenticationResponseValidatorTest.java | 74 ++++++-- .../integration/ReadmeIntegrationTest.java | 38 +++- .../sk/smartid/util/CallbackUrlUtilTest.java | 105 +++++++++++ .../util/UrlSafeTokenGeneratorTest.java | 78 ++++++++ 16 files changed, 804 insertions(+), 224 deletions(-) delete mode 100644 src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java create mode 100644 src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java create mode 100644 src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java rename src/main/java/ee/sk/smartid/{rest/dao/AuthenticationSessionRequest.java => exception/SessionSecretMismatchException.java} (80%) create mode 100644 src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java create mode 100644 src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java create mode 100644 src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 734072ff..75131838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.15] - 2025-09-17 +- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest +- Updated DeviceLinkAuthenticationResponseValidator to also validate userChallenge and userChallengeVerifier same device flows. + ## [3.1.14] - 2025-09-17 - Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 - Removed verificationCodeChoice interactions and related handling diff --git a/README.md b/README.md index b4991eef..172b897c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This library supports Smart-ID API v3.1. * [Generating QR-code](#generating-qr-code) * [Generate QR-code Data URI](#generate-qr-code-data-uri) * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) + * [Callback URL validation](#validating-callback-url) * [Querying sessions status](#session-status-request-handling-for-v31) * [Sessions status response](#session-status-response) * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) @@ -330,6 +331,54 @@ Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link authentication session with document number for Web2App flow + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating OK authentication sessions status response + +// Generate callback URL to be used for same device flows(Web2App, App2App) +CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("your-app://callback"); + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Set initial callback URL in the session request + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. +Jump to [Validate callback URL](#validating-callback-url) for more info about validating callback URL. + ### Device-link signature session #### Request Parameters @@ -632,6 +681,23 @@ BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toS // Return Data URI to frontend and display the QR-code String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); ``` +### Validating callback URL + +When using same device flows (Web2App or App2App) the initialCallbackUrl will be used by the Smart-ID app to redirect the user back to the Relying Party application. +Received callback URL will contain additional query parameters that must be validated by the Relying Party. + +Example of received callback URL for authentication: +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc&userChallengeVerifier=XtPfaGa8JnGtYrJjboooUf0KfY9sMEHrWFpSQrsUv9c` + +Example of received callback URL for signature or certificate choice +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc` + +1. RP must verify that the user sessions has `callbackUrl.urlToken()` with same value as in query parameter `value`. +2. RP must verify that the `sessionSecretDigest` query parameter matches the calculated digest created from session secret received in device link session init response. + For this library provides `CallbackUrlUtil.validateSessionSecretDigest(digestFromCallbackUrl, sessionSecret)` +3. For authentication same device flow RP also must verify the `userChallengeVerifier` query parameter. This can be done when polling the session status has finished and session status response has to be + validated. `deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, brokeredRpName);` + Value to validate `userChallengeVerifier` is in the session status response `signature.userChallenge`. ## Session status request handling for v3.1 diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java deleted file mode 100644 index 4c7d46d7..00000000 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseValidator.java +++ /dev/null @@ -1,169 +0,0 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Represents a template to validate authentication session status response. - *

    - * Use implementations {@link DeviceLinkAuthenticationResponseValidator} or {@link NotificationAuthenticationResponseValidator} - * to validate the flow specific authentication response. - * - * @param the type of authentication session request - */ -abstract class AuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; - private final SignatureValueValidator signatureValueValidator; - - protected AuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory, - SignatureValueValidator signatureValueValidator) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; - this.signatureValueValidator = signatureValueValidator; - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name used in the QR-code or device link - * @return the authentication identity - */ - public final AuthenticationIdentity validate(SessionStatus sessionStatus, - T authenticationSessionRequest, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, schemaName, null); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the authentication session status to be validated - * @param authenticationSessionRequest the authentication session request that was used to start the session - * @param schemaName the schema name used in the QR-code or device link - * @param brokeredRpName the brokered relying party name - * @return authentication identity containing details about the authenticated user - */ - public final AuthenticationIdentity validate(SessionStatus sessionStatus, - T authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - /** - * Constructs the payload used for signature validation. - * - * @param authenticationResponse the converted session status - * @param authenticationSessionRequest the authentication session request to start the session - * @param schemaName the schema name used in the QR-code or device link - * @param brokeredRpName the brokered relying party name - * @return the payload as a byte array - */ - protected abstract byte[] constructPayload(AuthenticationResponse authenticationResponse, - T authenticationSessionRequest, - String schemaName, - String brokeredRpName); - - /** - * Gets the requested certificate level from the authentication session request. - * - * @param authenticationSessionRequest the request to get certificate level from - * @return authentication certificate level - */ - protected abstract AuthenticationCertificateLevel getRequestedCertificateLevel(T authenticationSessionRequest); - - private void validateInputs(SessionStatus sessionStatus, T authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - T authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - protected static String toInteractionsBase64(String interactions) { - return Base64.getEncoder().encodeToString(calculateInteractionsDigest(interactions)); - } - - protected static String toBase64(String input) { - return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); - } - - private static byte[] calculateInteractionsDigest(String interactions) { - return DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - } - - private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } -} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index fa5c4247..764f1b97 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -27,16 +27,28 @@ */ import java.nio.charset.StandardCharsets; +import java.util.Base64; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.StringUtil; /** * Validates authentication response and converts it to {@link AuthenticationIdentity} */ -public class DeviceLinkAuthenticationResponseValidator extends AuthenticationResponseValidator { +public class DeviceLinkAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; /** * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} @@ -50,7 +62,10 @@ public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificat AuthenticationResponseMapper authenticationResponseMapper, SignatureValueValidator signatureValueValidator, AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - super(certificateValidator, authenticationResponseMapper, authenticationCertificatePurposeValidatorFactory, signatureValueValidator); + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; } /** @@ -67,18 +82,89 @@ public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertific new AuthenticationCertificatePurposeValidatorFactoryImpl()); } - @Override - protected AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @return Authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, null); + } + + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @param brokeredRpName the brokered RP name, used in the device link + * @return Authentication identity containing details about the authenticated user + * @throws UnprocessableSmartIdResponseException if the authentication response is invalid + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateUserChallenge(userChallengeVerifier, authenticationResponse); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { return authenticationSessionRequest == null ? AuthenticationCertificateLevel.QUALIFIED : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); } - @Override - protected byte[] constructPayload(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { String[] payload = { schemaName, SignatureProtocol.ACSP_V2.name(), @@ -87,13 +173,43 @@ protected byte[] constructPayload(AuthenticationResponse authenticationResponse, StringUtil.orEmpty(authenticationResponse.getUserChallenge()), toBase64(authenticationSessionRequest.relyingPartyName()), StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - toInteractionsBase64(authenticationSessionRequest.interactions()), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), authenticationResponse.getInteractionTypeUsed(), - StringUtil.orEmpty(authenticationSessionRequest.initialCallbackUrl()), + authenticationResponse.getFlowType() == FlowType.QR ? "" : authenticationSessionRequest.initialCallbackUrl(), authenticationResponse.getFlowType().getDescription() }; return String .join("|", payload) .getBytes(StandardCharsets.UTF_8); } + + private static void validateUserChallenge(String userChallengeVerifier, AuthenticationResponse authenticationResponse) { + if (authenticationResponse.getFlowType() != FlowType.WEB2APP + && authenticationResponse.getFlowType() != FlowType.APP2APP) { + return; + } + if (StringUtil.isEmpty(userChallengeVerifier)) { + throw new SmartIdClientException("Parameter 'userChallengeVerifier' must be provided for 'flowType' - " + authenticationResponse.getFlowType()); + } + String userChallenge = authenticationResponse.getUserChallenge(); + String urlUserChallenge = toDigest(userChallengeVerifier); + if (!userChallenge.equals(urlUserChallenge)) { + throw new UnprocessableSmartIdResponseException("Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"); + } + } + + private static String toDigest(String userChallengeVerifier) { + byte[] userChallengeVerifierDigest = DigestCalculator.calculateDigest(userChallengeVerifier.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(userChallengeVerifierDigest); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java index 32d9e8dd..b0df9e7e 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -27,16 +27,27 @@ */ import java.nio.charset.StandardCharsets; +import java.util.Base64; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.StringUtil; /** * Validates notification-based authentication session status */ -public class NotificationAuthenticationResponseValidator extends AuthenticationResponseValidator { +public class NotificationAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; /** * Creates an instance of {@link NotificationAuthenticationResponseValidator} @@ -48,8 +59,12 @@ public class NotificationAuthenticationResponseValidator extends AuthenticationR */ public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator, AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - super(certificateValidator, authenticationResponseMapper, authenticationCertificatePurposeValidatorFactory, signatureValueValidator); + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; } /** @@ -66,18 +81,83 @@ public static NotificationAuthenticationResponseValidator defaultSetupWithCertif new AuthenticationCertificatePurposeValidatorFactoryImpl()); } - @Override - protected AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name used in the QR-code or device link + * @return the authentication identity + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + *

    + * Should only be used for QR-code or notification-based authentication validation + * + * @param sessionStatus the authentication session status to be validated + * @param authenticationSessionRequest the authentication session request that was used to start the session + * @param schemaName the schema name used in the QR-code or device link + * @param brokeredRpName the brokered relying party name + * @return authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { return authenticationSessionRequest.certificateLevel() == null ? AuthenticationCertificateLevel.QUALIFIED : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); } - @Override - protected byte[] constructPayload(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { + private void validateSignature(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { String[] payload = { schemaName, SignatureProtocol.ACSP_V2.name(), @@ -86,7 +166,7 @@ protected byte[] constructPayload(AuthenticationResponse authenticationResponse, StringUtil.orEmpty(authenticationResponse.getUserChallenge()), toBase64(authenticationSessionRequest.relyingPartyName()), StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - toInteractionsBase64(authenticationSessionRequest.interactions()), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), authenticationResponse.getInteractionTypeUsed(), "", authenticationResponse.getFlowType().getDescription() @@ -95,4 +175,14 @@ protected byte[] constructPayload(AuthenticationResponse authenticationResponse, .join("|", payload) .getBytes(StandardCharsets.UTF_8); } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java new file mode 100644 index 00000000..3fd6c9d3 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java @@ -0,0 +1,38 @@ +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; + +/** + * Represents a callback URL with an associated URL-safe token. + * + * @param initialCallbackUri the full callback URI including the token as a query parameter + * @param urlToken the generated URL-safe token + */ +public record CallbackUrl(URI initialCallbackUri, String urlToken) { +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java new file mode 100644 index 00000000..4ff0cf07 --- /dev/null +++ b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java @@ -0,0 +1,85 @@ +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Generates URL-safe tokens using a cryptographically secure random number generator. + */ +public class UrlSafeTokenGenerator { + + private static final int MIN_NR_OF_CHARACTERS = 22; + private static final int MAX_NR_OF_CHARACTERS = 86; + + private UrlSafeTokenGenerator() { + } + + /** + * Generates a random URL-safe token between 22 and 86 characters long. + * + * @return a random URL-safe token with random size between the specified lengths + */ + public static String random() { + return randomBetween(MIN_NR_OF_CHARACTERS, MAX_NR_OF_CHARACTERS); + } + + /** + * Generates a random URL-safe token of the specified length. + * + * @param length the length of the token to generate (must be between 22 and 86) + * @return a random URL-safe token of the specified length + */ + public static String ofLength(int length) { + return randomBetween(length, length); + } + + /** + * Generates a random URL-safe token between the specified minimum and maximum lengths. + * + * @param minLen the minimum length of the token (must be between 22 and 86) + * @param maxLen the maximum length of the token (must be between 22 and 86) + * @return a random URL-safe token with random size between the specified lengths + * @throws SmartIdClientException if the specified lengths are out of bounds or invalid + */ + public static String randomBetween(int minLen, int maxLen) { + if (minLen < MIN_NR_OF_CHARACTERS || maxLen > MAX_NR_OF_CHARACTERS || minLen > maxLen) { + throw new SmartIdClientException("Length must be between 22 and 86 chars"); + } + SecureRandom secureRandom = new SecureRandom(); + // Random length between minLen and maxLen (inclusive) + int targetLen = secureRandom.nextInt(maxLen - minLen + 1) + minLen; + byte[] bytes = new byte[64]; + secureRandom.nextBytes(bytes); + String random = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + // Trim down to desired length + return random.substring(0, targetLen); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java similarity index 80% rename from src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java rename to src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java index 1fb847a8..f73b00aa 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.rest.dao; +package ee.sk.smartid.exception; /*- * #%L @@ -27,10 +27,11 @@ */ /** - * Marker interface for authentication session requests. - *

    - * Used to limit AuthenticationResponseValidator to - * only use authentication session request for validations. + * Thrown when the session secret digest from the callback does not match the calculated digest. */ -public interface AuthenticationSessionRequest { +public class SessionSecretMismatchException extends SmartIdException { + + public SessionSecretMismatchException(String message) { + super(message); + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java index a7d0b2be..abc502a4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java @@ -53,5 +53,5 @@ public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, String interactions, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable, AuthenticationSessionRequest { + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java index 6376f375..316bf206 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java @@ -52,5 +52,5 @@ public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, String interactions, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - String vcType) implements Serializable, AuthenticationSessionRequest { + String vcType) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java new file mode 100644 index 00000000..b12e1fc2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java @@ -0,0 +1,91 @@ +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Utility class for callback URL query parameter related operations. + */ +public final class CallbackUrlUtil { + + private CallbackUrlUtil() { + } + + /** + * Creates a callback URL by appending a random URL-safe token as a query parameter to the provided base URL. + * + * @param baseUrl the URL to which the token will be appended as a query parameter + * @return a {@link CallbackUrl} containing the full callback URL and the generated token + */ + public static CallbackUrl createCallbackUrl(String baseUrl) { + if (StringUtil.isEmpty(baseUrl)) { + throw new SmartIdClientException("Parameter for 'baseUrl' cannot be empty"); + } + String urlToken = UrlSafeTokenGenerator.random(); + return new CallbackUrl(UriBuilder.fromUri(baseUrl).queryParam("value", urlToken).build(), urlToken); + } + + /** + * Validates that the session secret digest from the callback URL matches the calculated digest of the provided session secret. + * + * @param sessionSecretDigest the session secret digest received in the callback URL + * @param sessionSecret the original session secret from the session initialization response + * @throws SmartIdClientException when any input parameters are empty + * @throws SessionSecretMismatchException when the session secrets do not match + */ + public static void validateSessionSecretDigest(String sessionSecretDigest, String sessionSecret) { + if (StringUtil.isEmpty(sessionSecretDigest)) { + throw new SmartIdClientException("Parameter for 'sessionSecretDigest' cannot be empty"); + } + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter for 'sessionSecret' cannot be empty"); + } + String calculatedSessionSecret = calculateDigest(sessionSecret); + if (!sessionSecretDigest.equals(calculatedSessionSecret)) { + throw new SessionSecretMismatchException("Session secret digest from callback does not match calculated session secret digest"); + } + } + + private static String calculateDigest(String sessionSecret) { + try { + byte[] decodedSessionSecret = Base64.getDecoder().decode(sessionSecret); + byte[] sessionSecretDigest = DigestCalculator.calculateDigest(decodedSessionSecret, HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(sessionSecretDigest); + } catch (IllegalArgumentException ex) { + throw new SmartIdClientException("Parameter 'sessionSecret' is not Base64-encoded value", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/util/InteractionUtil.java b/src/main/java/ee/sk/smartid/util/InteractionUtil.java index aa9ac2a3..2ba91cbd 100644 --- a/src/main/java/ee/sk/smartid/util/InteractionUtil.java +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -33,6 +33,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; import ee.sk.smartid.common.SmartIdInteraction; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.rest.dao.Interaction; @@ -63,6 +65,17 @@ public static String encodeToBase64(List interactions) { } } + /** + * Calculates SHA-256 digest of the interactions and encodes it to Base64 + * + * @param interactions interactions string + * @return base64 encoded SHA-256 digest + */ + public static String calculateDigest(String interactions){ + byte[] digest = DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getEncoder().encodeToString(digest); + } + /** * Checks if the list of interactions is empty or contains only null values * diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java index e22d369d..1f1ba798 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java @@ -74,13 +74,13 @@ void setUp() { deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); } - @Disabled("Can make this work when TEST numbers will be available in the DEMO env") + @Disabled("Will be fixed when testing with DEMO accounts will be possible") @Test - void validate() { + void validate_ok() { String rpChallenge = ""; SessionStatus sessionStatus = new SessionStatus(); DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -89,7 +89,19 @@ void validate() { assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); } - @Disabled("Can make this work when TEST numbers will be available in the DEMO env") + @Disabled + @Test + void validate_qrCodeWasUsedDoNotIncludeInitialCallbackUrlInSignatureValidation_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled + @Test + void validate_initialCallbackUrlWasUsed_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled("Will be fixed when testing with DEMO accounts will be possible") @Test void validate_certificateLevelHigherThanRequested_ok() { SessionStatus sessionStatus = new SessionStatus(); @@ -98,7 +110,7 @@ void validate_certificateLevelHigherThanRequested_ok() { sessionStatus.setCert(cert); var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo", null); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -112,51 +124,65 @@ class ValidateInputs { @Test void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); } @Test void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, null, "smart-id-demo", null)); assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, schemaName, null)); assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); } } @Test void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); } + @Nested + class ValidateUserChallenge { + + @ParameterizedTest + @NullAndEmptySource + void validate_sameDeviceFlowButUserChallengeVerifierNotProvided_throwException(String userChallengeVerifier) { + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", "", "Cjy8feLy_DB1GNF6lLpXf0VbzCMfTaLHzYOOpdXevSc", FlowType.WEB2APP); + + var ex = assertThrows(SmartIdClientException.class, + () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), userChallengeVerifier, "smart-id-demo", null)); + + assertEquals("Parameter 'userChallengeVerifier' must be provided for 'flowType' - WEB2APP", ex.getMessage()); + } + } + @Nested class ValidateSessionStatusCertificate { @Test void validate_certificateLevelLowerThanRequested_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", ""); + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", ""); - var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); } @Test void validate_certificateCannotBeUsedForAuthentication_throwException() { - var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", ""); + var sessionStatus = toSessionStatus(SIGN_CERT, "QUALIFIED", ""); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); } - } @Nested @@ -164,15 +190,25 @@ class ValidateAuthenticationSignature { @Test void validate_invalidSignature_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + var sessionStatus = toSessionStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); assertEquals("Signature value validation failed", ex.getMessage()); } } - private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue) { + return toSessionStatus(certificateValue, certificateLevel, signatureValue, "TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4", FlowType.QR); + } + + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue, + String userChallengeVerifier, + FlowType flowType) { var result = new SessionResult(); result.setEndResult("OK"); result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); @@ -192,9 +228,9 @@ private static SessionStatus toSessionsStatus(String certificateValue, String ce var signature = new SessionSignature(); signature.setServerRandom(toBase64("a".repeat(43))); - signature.setUserChallenge("TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4"); + signature.setUserChallenge(userChallengeVerifier); signature.setValue(toBase64("signatureValue")); - signature.setFlowType(FlowType.QR.getDescription()); + signature.setFlowType(flowType.getDescription()); signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index cdfd8767..72c53933 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -43,6 +43,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; @@ -79,6 +80,7 @@ import ee.sk.smartid.SmartIdDemoIntegrationTest; import ee.sk.smartid.TrustedCACertStore; import ee.sk.smartid.VerificationCodeCalculator; +import ee.sk.smartid.common.devicelink.CallbackUrl; import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.rest.SessionStatusPoller; @@ -92,6 +94,7 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.util.CallbackUrlUtil; @Disabled("Replace relying party UUID and name with your own values in setup") @SmartIdDemoIntegrationTest @@ -126,15 +129,20 @@ void anonymousAuthentication_withApp2App() { // Store generated rpChallenge only on backend side. Do not expose it to the client side. // Used for validating authentication sessions status OK response + // Create initial callback URL. + // Store the url-token only on backend side. Do not expose it to the client side. + // The url-token will be used to validate the callback request received from Smart-ID API + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + // Setup builder DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient .createDeviceLinkAuthentication() // to use anonymous authentication, do not set semantics identifier or document number .withRpChallenge(rpChallenge) - .withInitialCallbackUrl("https://example.com/callback") .withInteractions(Collections.singletonList( DeviceLinkInteraction.displayTextAndPin("Log in?") - )); + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Init authentication session DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); @@ -164,7 +172,7 @@ void anonymousAuthentication_withApp2App() { .withSessionToken(sessionToken) .withDigest(rpChallenge) .withLang("est") - .withInitialCallbackUrl("https://example.com/callback") + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()) .withInteractions(authenticationSessionRequest.interactions()) .buildDeviceLink(sessionSecret); @@ -175,12 +183,26 @@ void anonymousAuthentication_withApp2App() { // Check that the session has completed successfully assertEquals("COMPLETE", sessionStatus.getState()); + // Receive callback from Smart-ID API + // Extract query parameters from the callback URL received + Map queryParameters = Map.of("value", callbackUrl.urlToken(), "sessionSecretDigest", "asdjlaksdjklf", "userChallengeVerifier", "abachdfajklsfa"); + + // Validate there is active user session in the application with matching url-token + String tokenInUrl = queryParameters.get("value"); + + // Validate that sessionSecretDigest in the callback URL validates against sessionSecret from the init session response + CallbackUrlUtil.validateSessionSecretDigest(queryParameters.get("sessionSecretDigest"), sessionSecret); + // Set up AuthenticationResponseValidator TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, builder.getAuthenticationSessionRequest(), "smart-id-demo"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate( + sessionStatus, + builder.getAuthenticationSessionRequest(), + queryParameters.get("userChallengeVerifier"), + "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -259,7 +281,7 @@ void authentication_withSemanticIdentifierAndQrCode() { TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -326,7 +348,7 @@ void authentication_withDocumentNumberAndQrCode() { TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); assertEquals("40504040001", authenticationIdentity.getIdentityCode()); assertEquals("OK", authenticationIdentity.getGivenName()); @@ -788,6 +810,9 @@ void signing_withQrCode() { // Will be used to calculate elapsed time being used in device link and in authCode Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Build the device link URI // This base URI will be used for QR code or App2App flows URI deviceLink = smartIdClient.createDynamicContent() @@ -795,6 +820,7 @@ void signing_withQrCode() { .withDeviceLinkType(DeviceLinkType.QR_CODE) .withSessionType(SessionType.CERTIFICATE_CHOICE) .withSessionToken(sessionToken) + .withElapsedSeconds(elapsedSeconds) .withLang("est") .buildDeviceLink(sessionSecret); diff --git a/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java new file mode 100644 index 00000000..fd1b36f9 --- /dev/null +++ b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java @@ -0,0 +1,105 @@ +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class CallbackUrlUtilTest { + + private static final String SESSION_SECRET_DIGEST = "nKMc7gT3mvWuJtfXVFjCY2ehuvTs26f1Sgjk6g9oOr8"; + + @Nested + class CreateCallbackUrl { + + @Test + void createCallbackUrl_valueQueryParameterIsSameAsUrlToken() { + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + + assertEquals("https://example.com/callback?value=" + callbackUrl.urlToken(), + callbackUrl.initialCallbackUri().toString()); + } + + @ParameterizedTest + @NullAndEmptySource + void createCallbackUrl_inputBaseUrlIsEmpty_throwException(String baseUrl) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.createCallbackUrl(baseUrl)); + assertEquals("Parameter for 'baseUrl' cannot be empty", ex.getMessage()); + } + } + + @Nested + class ValidateSessionSecretDigest { + + @Test + void validateSessionSecretDigest() { + String sessionSecret = "fBo1/L1vM9xcSmZF7hvvooEj"; + assertDoesNotThrow(() -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretDigestIsEmpty_throwException(String sessionSecretDigest) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(sessionSecretDigest, "")); + assertEquals("Parameter for 'sessionSecretDigest' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretIsEmpty_throwException(String sessionSecret) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Parameter for 'sessionSecret' cannot be empty", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretValidationFails_throwException() { + String sessionSecret = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + + var ex = assertThrows(SessionSecretMismatchException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Session secret digest from callback does not match calculated session secret digest", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretIsNotBase64Encoded_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, "sessionSecret")); + assertEquals("Parameter 'sessionSecret' is not Base64-encoded value", ex.getMessage()); + } + } +} diff --git a/src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java b/src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java new file mode 100644 index 00000000..4e8c6948 --- /dev/null +++ b/src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java @@ -0,0 +1,78 @@ +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class UrlSafeTokenGeneratorTest { + + @Test + void random() { + String random = UrlSafeTokenGenerator.random(); + + assertTrue(random.length() >= 22 && random.length() <= 86); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void ofLength() { + String random = UrlSafeTokenGenerator.ofLength(22); + + assertEquals(22, random.length()); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void randomBetween() { + String random = UrlSafeTokenGenerator.randomBetween(22, 24); + + assertTrue(random.length() >= 22 && random.length() <= 24); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @ParameterizedTest + @CsvSource({ + "21, 86", // min length smaller than allowed + "22, 87", // max length larger than allowed + "86, 22" // min length larger than max length + }) + void randomBetween(int minLength, int maxLength) { + var ex = assertThrows(SmartIdClientException.class, () -> UrlSafeTokenGenerator.randomBetween(minLength, maxLength)); + assertEquals("Length must be between 22 and 86 chars", ex.getMessage()); + } +} From ae75afa214fe03608be51cfa34cea9ec3ac0358a Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Wed, 8 Oct 2025 09:51:54 +0300 Subject: [PATCH 52/57] Update notification based certificate choice to use v3.1 endpoint (#140) * SLIB-126 - add util to create callbackUrl with url-token * SLIB-126 - move UrlSafeTokenGenerator and CallbackUrl to common package * SLIB-113 - update notification-based certificate choice path and request object; update relying party UUID used in test data * SLIB-113 - update notification-based certificate choice builder validations to new standard; activate integrations tests with DEMO environment for notification-based flows * SLIB-113 - improve notification-based certificate choice example and Readme test * SLIB-113 - add handling for ACCOUNT_UNUSABLE end result * SLIB-113 - update notification-based certificate choice builder to use SmartIdRequestSetupException and improve javadoc * SLIB-113 - move UrlSafeTokenGeneratorTest to correct package; fix incorrect validation * SLIB-113 - remove todos * SLIB-113 - improve code style in NotificationCertificateChoiceSessionRequestBuilderTest --- CHANGELOG.md | 4 + README.md | 10 +- ...ertificateChoiceSessionRequestBuilder.java | 10 +- .../ee/sk/smartid/ErrorResultHandler.java | 2 + ...ertificateChoiceSessionRequestBuilder.java | 63 ++---- .../UserAccountUnusableException.java | 37 ++++ .../ee/sk/smartid/rest/SmartIdConnector.java | 17 +- .../sk/smartid/rest/SmartIdRestConnector.java | 9 +- ...ceLinkCertificateChoiceSessionRequest.java | 54 +++++ ...ationCertificateChoiceSessionRequest.java} | 15 +- ...ficateChoiceSessionRequestBuilderTest.java | 40 ++-- ...ficateChoiceSessionRequestBuilderTest.java | 204 ++++++++---------- ...essionEndResultErrorArgumentsProvider.java | 4 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 26 ++- .../UrlSafeTokenGeneratorTest.java | 3 +- .../integration/ReadmeIntegrationTest.java | 13 +- .../SmartIdRestIntegrationTest.java | 16 +- .../rest/SmartIdRestConnectorTest.java | 201 +++++++++++++---- ...ation-session-request-invalid-request.json | 2 +- ...uthentication-session-request-qr-code.json | 2 +- ...ession-request-same-device-all-fields.json | 2 +- ...uest-same-device-only-required-fields.json | 2 +- ...entication-session-request-all-fields.json | 2 +- ...uthentication-session-request-invalid.json | 2 +- ...-session-request-only-required-fields.json | 2 +- ...by-document-number-request-all-fields.json | 2 +- ...t-number-request-only-required-fields.json | 2 +- ...device-link-signature-request-qr-code.json | 2 +- ...ce-link-signature-request-same-device.json | 2 +- ...ate-choice-session-request-all-fields.json | 2 +- ...te-choice-session-request-device-link.json | 2 +- ...te-choice-session-request-for-qr-code.json | 2 +- ...-signature-session-request-all-fields.json | 2 +- ...-session-request-only-required-fields.json | 2 +- ...ate-choice-session-request-all-fields.json | 9 + ...e-session-request-invalid-credentials.json | 4 + ...ficate-choice-session-request-invalid.json | 3 + ...-session-request-only-required-fields.json | 4 + .../certificate-choice-session-request.json | 6 - ...n-certificate-choice-session-response.json | 4 - .../session-status-account-unusable.json | 8 + ...n-certificate-choice-session-response.json | 4 + 42 files changed, 513 insertions(+), 289 deletions(-) create mode 100644 src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java rename src/main/java/ee/sk/smartid/rest/dao/{CertificateChoiceSessionRequest.java => NotificationCertificateChoiceSessionRequest.java} (71%) rename src/test/java/ee/sk/smartid/{util => common/devicelink}/UrlSafeTokenGeneratorTest.java (96%) create mode 100644 src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json create mode 100644 src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json create mode 100644 src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json create mode 100644 src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json delete mode 100644 src/test/resources/requests/sign/notification/certificate-choice-session-request.json delete mode 100644 src/test/resources/responses/notification-certificate-choice-session-response.json create mode 100644 src/test/resources/responses/session-status-account-unusable.json create mode 100644 src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 75131838..a39360de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.16] - 2025-10-04 +- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint +- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response + ## [3.1.15] - 2025-09-17 - Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest - Updated DeviceLinkAuthenticationResponseValidator to also validate userChallenge and userChallengeVerifier same device flows. diff --git a/README.md b/README.md index 172b897c..04fe968f 100644 --- a/README.md +++ b/README.md @@ -1178,9 +1178,6 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ### Notification-based certificate choice session -> [!CAUTION] -> The notification-based certificate choice has not yet been updated to be used with Smart-ID API v3.1 - #### Request parameters * `relyingPartyUUID`: Required. UUID of the Relying Party. @@ -1206,10 +1203,12 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) +// Use requested certificate level to validate certificate choice session status OK response. +CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client .createNotificationCertificateChoice() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withCertificateLevel(requestedCertificateLevel) .initCertificateChoice(); String sessionId = certificateChoiceSessionResponse.sessionID(); @@ -1431,6 +1430,7 @@ Exception Categories These exceptions handle issues related to the user's Smart-ID account or session requirements. * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. + * `UserAccountUnusableException` Thrown when the user's Smart-ID account is not currently usable for the requested operation. * Validation and Parsing Exceptions These exceptions arise during validation or parsing operations within the library. * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. @@ -1499,7 +1499,7 @@ ResteasyClient resteasyClient = new ResteasyClientBuilder() .build(); SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); client.setRelyingPartyName("DEMO"); client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); client.setConfiguredClient(resteasyClient); diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index 13beadac..f3d98dba 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -33,7 +33,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.util.StringUtil; @@ -150,8 +150,8 @@ public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(S */ public DeviceLinkSessionResponse initCertificateChoice() { validateRequestParameters(); - CertificateChoiceSessionRequest certificateChoiceSessionRequest = createCertificateRequest(); - DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(certificateChoiceSessionRequest); + DeviceLinkCertificateChoiceSessionRequest deviceLinkCertificateChoiceSessionRequest = createCertificateRequest(); + DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(deviceLinkCertificateChoiceSessionRequest); validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); return deviceLinkCertificateChoiceSessionResponse; } @@ -169,8 +169,8 @@ private void validateRequestParameters() { validateInitialCallbackUrl(); } - private CertificateChoiceSessionRequest createCertificateRequest() { - return new CertificateChoiceSessionRequest( + private DeviceLinkCertificateChoiceSessionRequest createCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index e628775b..875dedd0 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -35,6 +35,7 @@ import ee.sk.smartid.exception.permanent.SmartIdServerException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; @@ -77,6 +78,7 @@ public static void handle(SessionResult sessionResult) { case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); case "SERVER_ERROR" -> throw new SmartIdServerException(); + case "ACCOUNT_UNUSABLE" -> throw new UserAccountUnusableException(); default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); } } diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index ab4d1174..e5afc41c 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -28,23 +28,23 @@ import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.util.StringUtil; +/** + * Builder for notification-based certificate choice session requests + */ public class NotificationCertificateChoiceSessionRequestBuilder { - private static final Logger logger = LoggerFactory.getLogger(NotificationCertificateChoiceSessionRequestBuilder.class); - private final SmartIdConnector connector; + private String relyingPartyUUID; private String relyingPartyName; private CertificateLevel certificateLevel; @@ -142,72 +142,53 @@ public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifie } /** - * Sends the notification request and get the init session response - *

    - * There are 2 supported ways to start authentication session: - *

      - *
    • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
    • - *
    + * Initializes a notification-based certificate choice session * * @return init session response + * @throws SmartIdRequestSetupException whe the provided request parameters are invalid + * @throws UnprocessableSmartIdResponseException when the response is missing required parameters + * @throws SmartIdClientException when the request could not be sent */ public NotificationCertificateChoiceSessionResponse initCertificateChoice() { validateRequestParameters(); - CertificateChoiceSessionRequest request = createCertificateChoiceRequest(); + NotificationCertificateChoiceSessionRequest request = createCertificateChoiceRequest(); NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); validateResponseParameters(notificationCertificateChoiceSessionResponse); return notificationCertificateChoiceSessionResponse; } - private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(CertificateChoiceSessionRequest request) { + private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(NotificationCertificateChoiceSessionRequest request) { if (semanticsIdentifier == null) { - throw new SmartIdClientException("SemanticsIdentifier must be set."); + throw new SmartIdRequestSetupException("Value for 'semanticIdentifier' must be set"); } return connector.initNotificationCertificateChoice(request, semanticsIdentifier); } private void validateRequestParameters() { if (StringUtil.isEmpty(relyingPartyUUID)) { - logger.error("Parameter relyingPartyUUID must be set"); - throw new SmartIdClientException("Parameter relyingPartyUUID must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } if (StringUtil.isEmpty(relyingPartyName)) { - logger.error("Parameter relyingPartyName must be set"); - throw new SmartIdClientException("Parameter relyingPartyName must be set"); + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); } - validateNonce(); } - private CertificateChoiceSessionRequest createCertificateChoiceRequest() { - return new CertificateChoiceSessionRequest( + private NotificationCertificateChoiceSessionRequest createCertificateChoiceRequest() { + return new NotificationCertificateChoiceSessionRequest( relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, nonce, capabilities, - shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null, - null - ); - } - - private void validateNonce() { - if (nonce == null) { - return; - } - if (nonce.isEmpty()) { - logger.error("Parameter nonce value has to be at least 1 character long"); - throw new SmartIdClientException("Parameter nonce value has to be at least 1 character long"); - } - if (nonce.length() > 30) { - logger.error("Nonce cannot be longer that 30 chars"); - throw new SmartIdClientException("Nonce cannot be longer that 30 chars"); - } + shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null); } private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); } } } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java new file mode 100644 index 00000000..cc17173c --- /dev/null +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java @@ -0,0 +1,37 @@ +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + + +public class UserAccountUnusableException extends UserAccountException { + + public UserAccountUnusableException() { + super("The account is currently unusable"); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 96805701..819184eb 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -33,17 +33,18 @@ import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureSessionRequest; @@ -72,7 +73,7 @@ public interface SmartIdConnector extends Serializable { * @param request CertificateChoiceSessionRequest containing necessary parameters * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoiceSessionRequest request); + DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request); /** * Initiates a linked notification based signature session. @@ -86,15 +87,15 @@ public interface SmartIdConnector extends Serializable { * Initiates a notification based certificate choice request. * * @param request CertificateChoiceSessionRequest containing necessary parameters - * @param semanticsIdentifier The semantics identifier + * @param semanticsIdentifier The semantics identifier to be used for the session * @return NotificationCertificateChoiceSessionResponse containing sessionID */ - NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); + NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Queries signing certificate by document number. * - * @param request CertificateByDocumentNumberRequest containing necessary parameters + * @param request CertificateByDocumentNumberRequest containing necessary parameters * @param documentNumber The document number * @return CertificateResponse containing response state and certificate information. */ diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index f648ffca..4ecbdbb2 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -45,7 +45,7 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; @@ -53,6 +53,7 @@ import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; @@ -85,7 +86,7 @@ public class SmartIdRestConnector implements SmartIdConnector { private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; - private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "/certificatechoice/notification/etsi"; + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "signature/certificate-choice/notification/etsi"; private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; @@ -186,7 +187,7 @@ public NotificationAuthenticationSessionResponse initNotificationAuthentication( } @Override - public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(CertificateChoiceSessionRequest request) { + public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request) { logger.debug("Initiating device link based certificate choice request"); URI uri = UriBuilder .fromUri(endpointUrl) @@ -207,7 +208,7 @@ public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSign } @Override - public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(CertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java new file mode 100644 index 00000000..0bfd03e5 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java @@ -0,0 +1,54 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request to create a Device Link session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + * @param initialCallbackUrl Initial callback URL to be used instead of the default one configured for the RP. + */ +public record DeviceLinkCertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { + +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java similarity index 71% rename from src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java rename to src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java index e203a1fa..f4c67f6c 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java @@ -31,13 +31,22 @@ import com.fasterxml.jackson.annotation.JsonInclude; -public record CertificateChoiceSessionRequest( +/** + * Request to create a notification-based session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + */ +public record NotificationCertificateChoiceSessionRequest( String relyingPartyUUID, String relyingPartyName, @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index 616ca6c9..deba8ef4 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -52,7 +52,7 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; class DeviceLinkCertificateChoiceSessionRequestBuilderTest { @@ -74,7 +74,7 @@ void setUp() { @Test void initiateCertificateChoice() { - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); @@ -84,13 +84,13 @@ void initiateCertificateChoice() { assertEquals("test-session-secret", result.sessionSecret()); assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullRequestProperties() { builderService.withShareMdClientIpAddress(false); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); @@ -100,24 +100,24 @@ void initiateCertificateChoice_nullRequestProperties() { assertEquals("test-session-secret", result.sessionSecret()); assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_missingCertificateLevel() { builderService.withCertificateLevel(null); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_withValidCapabilities() { builderService.withCapabilities("ADVANCED", "QUALIFIED"); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); @@ -127,13 +127,13 @@ void initiateCertificateChoice_withValidCapabilities() { assertEquals("test-session-secret", result.sessionSecret()); assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullCapabilities() { builderService.withCapabilities(); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); @@ -143,7 +143,7 @@ void initiateCertificateChoice_nullCapabilities() { assertEquals("test-session-secret", result.sessionSecret()); assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Nested @@ -157,7 +157,7 @@ void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { "test-session-secret", URI.create("https://example.com/device-link"), null); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); @@ -171,7 +171,7 @@ void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken "test-session-secret", URI.create("https://example.com/device-link")); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); @@ -185,7 +185,7 @@ void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecr sessionSecret, URI.create("https://example.com/device-link")); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); @@ -199,7 +199,7 @@ void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) "test-session-secret", uriString == null ? null : URI.create(uriString)); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(response); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); @@ -207,7 +207,7 @@ void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) @Test void initiateCertificateChoice_userAccountNotFound() { - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); assertEquals(UserAccountNotFoundException.class, ex.getClass()); @@ -241,23 +241,23 @@ void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { @Test void initiateCertificateChoice_withoutInitialCallbackUrl() { builderService.withInitialCallbackUrl(null); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @Test void initiateCertificateChoice_nullNonce() { builderService.withNonce(null); - when(connector.initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); DeviceLinkSessionResponse result = builderService.initCertificateChoice(); assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(CertificateChoiceSessionRequest.class)); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } @ParameterizedTest diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index f06714ab..797f59dd 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -37,9 +37,9 @@ import java.util.Collections; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Stream; -import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Nested; @@ -53,14 +53,19 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; class NotificationCertificateChoiceSessionRequestBuilderTest { + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + private SmartIdConnector connector; @BeforeEach @@ -68,109 +73,82 @@ void setUp() { connector = mock(SmartIdConnector.class); } - @Nested - class ValidateRequiredRequestParameters { + @Test + void initCertificateChoiceSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); - @Test - void initCertificateChoiceSession_withSemanticsIdentifier() { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); + toBaseNotificationCertChoiceRequestBuilder() + .initCertificateChoice(); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101")) - .initCertificateChoice(); + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } + @Nested + class ValidateRequiredRequestParameters { @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedCertificateLevel) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createCertificateChoiceSessionResponse()); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(certificateLevel) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + toNotificationCertChoiceRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) .initCertificateChoice(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - CertificateChoiceSessionRequest request = requestCaptor.getValue(); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedValue, request.certificateLevel()); + assertEquals(expectedCertificateLevel, request.certificateLevel()); } @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) void initCertificateChoiceSession_nonce_ok(String nonce) { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createCertificateChoiceSessionResponse()); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(nonce) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(nonce)) .initCertificateChoice(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - CertificateChoiceSessionRequest request = requestCaptor.getValue(); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.nonce()); } @Test void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createCertificateChoiceSessionResponse()); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .initCertificateChoice(); + toBaseNotificationCertChoiceRequestBuilder().initCertificateChoice(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - CertificateChoiceSessionRequest request = requestCaptor.getValue(); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); assertNull(request.requestProperties()); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void initCertificateChoiceSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + void initCertificateChoiceSession_ipQueryingSet_ok(boolean ipRequested) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createCertificateChoiceSessionResponse()); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withShareMdClientIpAddress(ipRequested) + toNotificationCertChoiceRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) .initCertificateChoice(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - CertificateChoiceSessionRequest request = requestCaptor.getValue(); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); assertNotNull(request.requestProperties()); assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); @@ -179,20 +157,15 @@ void initCertificateChoiceSession_ipQueryingRequired_ok(boolean ipRequested) { @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(createCertificateChoiceSessionResponse()); - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .withCapabilities(capabilities) + toNotificationCertChoiceRequestBuilder(b -> b.withCapabilities(capabilities)) .initCertificateChoice(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CertificateChoiceSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - CertificateChoiceSessionRequest request = requestCaptor.getValue(); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); assertEquals(expectedCapabilities, request.capabilities()); } @@ -200,47 +173,40 @@ void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName("DEMO") - .initCertificateChoice()); - assertEquals("Parameter relyingPartyUUID must be set", exception.getMessage()); + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); } @ParameterizedTest @NullAndEmptySource void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName(relyingPartyName) - .initCertificateChoice()); - assertEquals("Parameter relyingPartyName must be set", exception.getMessage()); + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); } @ParameterizedTest @ArgumentsSource(InvalidNonceProvider.class) void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(invalidNonce) - .initCertificateChoice()); + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(invalidNonce)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); assertEquals(expectedException, exception.getMessage()); } @Test - void initCertificateChoiceSession_semanticsIdentifierOrDocumentNumberMissing_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .initCertificateChoice()); - assertEquals("SemanticsIdentifier must be set.", exception.getMessage()); + void initCertificateChoiceSession_semanticsIdentifierMissing_throwException() { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'semanticIdentifier' must be set", exception.getMessage()); } } @@ -250,27 +216,33 @@ class ValidateRequiredResponseParameters { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var exception = assertThrows(SmartIdClientException.class, () -> { - var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); - notificationCertificateChoiceSessionResponse.setSessionID(sessionId); - when(connector.initNotificationCertificateChoice(any(CertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); - - new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-1234567890")) - .initCertificateChoice(); - }); - assertEquals("Session ID is missing from the response", exception.getMessage()); + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); + notificationCertificateChoiceSessionResponse.setSessionID(sessionId); + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); + NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initCertificateChoice); + assertEquals("Notification-based certificate choice response field 'sessionID' is missing or empty", exception.getMessage()); } } private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); - notificationCertificateChoiceSessionResponse.setSessionID("00000000-0000-0000-0000-000000000000"); + notificationCertificateChoiceSessionResponse.setSessionID(RELYING_PARTY_UUID); return notificationCertificateChoiceSessionResponse; } + private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationCertChoiceRequestBuilder()); + } + + private NotificationCertificateChoiceSessionRequestBuilder toBaseNotificationCertChoiceRequestBuilder() { + return new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); + } + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { @@ -295,8 +267,8 @@ private static class CapabilitiesArgumentProvider implements ArgumentsProvider { public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(new String[0], Collections.emptySet()), - Arguments.of(new String[]{"ADVANCED"}, Set.of("ADVANCED")), - Arguments.of(new String[]{"ADVANCED", "QUALIFIED"}, Set.of("ADVANCED", "QUALIFIED")) + Arguments.of(new String[]{"capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")) ); } } @@ -305,8 +277,8 @@ private static class InvalidNonceProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of(Named.of("Empty string as value", ""), "Parameter nonce value has to be at least 1 character long"), - Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Nonce cannot be longer that 30 chars") + Arguments.of(Named.of("Empty string as value", ""), "Value for 'nonce' length must be between 1 and 30 characters"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Value for 'nonce' length must be between 1 and 30 characters") ); } } diff --git a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java index eb28a30c..4eb6df4e 100644 --- a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -38,6 +38,7 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.useraccount.DocumentUnusableException; import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; import ee.sk.smartid.exception.useraction.SessionTimeoutException; import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; import ee.sk.smartid.exception.useraction.UserRefusedException; @@ -57,7 +58,8 @@ public Stream provideArguments(ExtensionContext context) { Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), Arguments.of("SERVER_ERROR", SmartIdServerException.class), - Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class) + Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class), + Arguments.of("ACCOUNT_UNUSABLE", UserAccountUnusableException.class) ); } } diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 5e599891..13681fc5 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -71,7 +71,7 @@ class SmartIdClientTest { @BeforeEach void setUp() { smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); smartIdClient.setRelyingPartyName("DEMO"); smartIdClient.setHostUrl("http://localhost:18089"); smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); @@ -142,15 +142,29 @@ void createQrCodeCertificateChoiceSession() { class NotificationCertificateChoiceSession { @Test - void createNotificationCertificateChoice_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/certificatechoice/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/certificate-choice-session-request.json", - "responses/notification-certificate-choice-session-response.json"); + void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .initCertificateChoice(); + + assertNotNull(response.getSessionID()); + } + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) + .withCertificateLevel(CertificateLevel.QUALIFIED) .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withShareMdClientIpAddress(true) .initCertificateChoice(); assertNotNull(response.getSessionID()); diff --git a/src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java similarity index 96% rename from src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java rename to src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java index 4e8c6948..69adad3c 100644 --- a/src/test/java/ee/sk/smartid/util/UrlSafeTokenGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.util; +package ee.sk.smartid.common.devicelink; /*- * #%L @@ -36,7 +36,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; import ee.sk.smartid.exception.permanent.SmartIdClientException; class UrlSafeTokenGeneratorTest { diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 72c53933..07c05060 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -96,7 +96,6 @@ import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.CallbackUrlUtil; -@Disabled("Replace relying party UUID and name with your own values in setup") @SmartIdDemoIntegrationTest public class ReadmeIntegrationTest { @@ -107,7 +106,7 @@ public class ReadmeIntegrationTest { @BeforeEach void setUp() { smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-0000-0000-000000000000"); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); smartIdClient.setRelyingPartyName("DEMO"); smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); @@ -115,7 +114,7 @@ void setUp() { smartIdClient.setTrustStore(keyStore); } - @Disabled("These test for created for test-accounts in demo, but these are not currently available device-link flows") + @Disabled("Testing with device-link demo accounts is not possible at the moment") @Nested class DeviceLinkBasedExamples { @@ -663,10 +662,12 @@ void certificateChoice_withSemanticIdentifier() { SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) + // Use requested certificate level to validate certificate choice session status OK response. + CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient .createNotificationCertificateChoice() .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withCertificateLevel(requestedCertificateLevel) .initCertificateChoice(); String sessionId = certificateChoiceSessionResponse.getSessionID(); @@ -681,7 +682,7 @@ void certificateChoice_withSemanticIdentifier() { TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); assertEquals("OK", response.getEndResult()); assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); @@ -689,6 +690,7 @@ void certificateChoice_withSemanticIdentifier() { assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); } + @Disabled("Not yet updated to work with v3.1") @Test void signature_withSemanticsIdentifier() { var semanticIdentifier = new SemanticsIdentifier( @@ -787,6 +789,7 @@ void queryCertificate() { } } + @Disabled("Testing with device-link demo accounts is not possible at the moment") @Nested class LinkedNotificationBasedSignatureSession { diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 2ed6728c..317c97b0 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -45,16 +45,18 @@ import ee.sk.smartid.SignatureAlgorithm; import ee.sk.smartid.SignatureProtocol; import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.VerificationCodeType; import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.SmartIdRestConnector; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; @@ -64,12 +66,11 @@ import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.InteractionUtil; -@Disabled("Relying party demo account not yet available for v3") @SmartIdDemoIntegrationTest class SmartIdRestIntegrationTest { // Replace these to test with V3 - private static final String RELYING_PARTY_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; private static final String RELYING_PARTY_NAME = "DEMO"; private static final String UUID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; @@ -150,7 +151,7 @@ class CertificateChoice { @Test void initDeviceLinkCertificateChoice() { - var request = new CertificateChoiceSessionRequest( + var request = new DeviceLinkCertificateChoiceSessionRequest( RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, @@ -245,7 +246,7 @@ void initNotificationAuthentication_withSemanticIdentifier() { void initNotificationAuthentication_withDocumentNumber() { var request = toAuthenticationRequest(); - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, "PNOEE-40504040001-DEMO-Q"); assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); } @@ -264,7 +265,7 @@ private static NotificationAuthenticationSessionRequest toAuthenticationRequest( InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), new RequestProperties(true), null, - "numeric4" + VerificationCodeType.NUMERIC4.getValue() ); } } @@ -274,7 +275,7 @@ class CertificateChoice { @Test void initNotificationCertificateChoice_withSemanticIdentifier() { - var request = new CertificateChoiceSessionRequest(RELYING_PARTY_NAME, RELYING_PARTY_UUID, null, null, null, null, null); + var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, new SemanticsIdentifier("PNOEE-40504040001")); @@ -282,6 +283,7 @@ void initNotificationCertificateChoice_withSemanticIdentifier() { } } + @Disabled @Nested class Signature { diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 0973026c..b4b4e792 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -34,6 +34,7 @@ import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubStrictRequestWithResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -71,15 +72,16 @@ import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; @@ -299,6 +301,13 @@ void getSessionStatus_serverError() { assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); } + @Test + void getSessionStatus_accountUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-account-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("ACCOUNT_UNUSABLE", sessionStatus.getResult().getEndResult()); + } + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); @@ -785,7 +794,7 @@ void initNotificationAuthentication_allFields_ok() { void initNotificationAuthentication_badRequest_throwException() { SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); - var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-0000-0000-000000000000", + var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-4000-8000-000000000000", "DEMO", null, null, @@ -872,7 +881,7 @@ public void setUp() { void initDeviceLinkCertificateChoice() { stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); Instant start = Instant.now(); DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); Instant end = Instant.now(); @@ -882,7 +891,7 @@ void initDeviceLinkCertificateChoice() { @Test void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); @@ -893,7 +902,7 @@ void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestExc void initDeviceLinkCertificateChoice_userAccountNotFound() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -901,7 +910,7 @@ void initDeviceLinkCertificateChoice_userAccountNotFound() { void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -909,7 +918,7 @@ void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { void initDeviceLinkCertificateChoice_invalidRequest() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - var request = new CertificateChoiceSessionRequest("", "", null, null, null, null, null); + var request = new DeviceLinkCertificateChoiceSessionRequest("", "", null, null, null, null, null); assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -918,7 +927,7 @@ void initDeviceLinkCertificateChoice_invalidRequest() { void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); @@ -928,7 +937,7 @@ void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationExcep void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -937,7 +946,7 @@ void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFound void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } @@ -946,7 +955,7 @@ void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalExceptio void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); @@ -956,14 +965,14 @@ void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); } - private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { - return new CertificateChoiceSessionRequest( - "00000000-0000-0000-0000-000000000000", + private static DeviceLinkCertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED", null, @@ -1088,7 +1097,7 @@ private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(Cer "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", "rsassa-pss", new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); - return new LinkedSignatureSessionRequest("00000000-0000-0000-0000-000000000000", + return new LinkedSignatureSessionRequest("00000000-0000-4000-8000-000000000000", "DEMO", certificateLevel != null ? certificateLevel.name() : null, "RAW_DIGEST_SIGNATURE", @@ -1105,7 +1114,7 @@ private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(Cer @WireMockTest(httpPort = 18089) class SemanticsIdentifierNotificationCertificateChoiceTests { - private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/certificatechoice/notification/etsi/PNOEE-31111111111"; + private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/signature/certificate-choice/notification/etsi/PNOEE-31111111111"; private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); private SmartIdRestConnector connector; @@ -1117,10 +1126,37 @@ public void setUp() { } @Test - void initCertificateChoice_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "responses/notification-certificate-choice-session-response.json"); + void initCertificateChoice_onlyRequiredFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - CertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + } + + @Test + void initCertificateChoice_allFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + var request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + "QUALIFIED", + "cmFuZG9tTm9uY2U=", + null, + new RequestProperties(true)); SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); @@ -1130,33 +1166,118 @@ void initCertificateChoice_withSemanticsIdentifier_successful() { } @Test - void initCertificateChoice_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/certificate-choice-session-request.json"); + void initCertificateChoice_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json"); - assertThrows(UserAccountNotFoundException.class, - () -> connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), SEMANTICS_IDENTIFIER)); + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); } @Test - void initCertificateChoice_requestIsUnauthorized_throwException() { + void initCertificateChoice_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_rpDoesNotHavePermission_throwException() { SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/certificate-choice-session-request.json"); + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initNotificationCertificateChoice(toCertificateChoiceSessionRequest(), SEMANTICS_IDENTIFIER)); + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); } - private static CertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { - return new CertificateChoiceSessionRequest( - "00000000-0000-0000-0000-000000000000", + @Test + void initCertificateChoice_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", "DEMO", - "ADVANCED", - "cmFuZG9tTm9uY2U=", null, null, - null - ); + null, + null); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 471); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_userShouldCheckPortal_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 472); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_javaClientBeingUsedIsTooOld_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 480); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 580); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); } } @@ -1191,7 +1312,7 @@ void getCertificateByDocumentNumber_successful() { void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); - var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", null); + var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", null); CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); assertNotNull(response); @@ -1539,7 +1660,7 @@ private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthentication "rsassa-pss", new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new DeviceLinkAuthenticationSessionRequest( - "00000000-0000-0000-0000-000000000000", + "00000000-0000-4000-8000-000000000000", "DEMO", CertificateLevel.QUALIFIED.name(), SignatureProtocol.ACSP_V2, @@ -1558,7 +1679,7 @@ private static NotificationAuthenticationSessionRequest toNotificationAuthentica new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); return new NotificationAuthenticationSessionRequest( - "00000000-0000-0000-0000-000000000000", + "00000000-0000-4000-8000-000000000000", "DEMO", certificateLevel != null ? certificateLevel.name() : null, SignatureProtocol.ACSP_V2.name(), @@ -1571,7 +1692,7 @@ private static NotificationAuthenticationSessionRequest toNotificationAuthentica } private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { - return new CertificateByDocumentNumberRequest("00000000-0000-0000-0000-000000000000", "DEMO", "ADVANCED"); + return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); } private static SignatureSessionRequest createSignatureSessionRequest() { diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json index cabe10ef..58621ea6 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", "certificateLevel": "QUALIFIED", diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json index ecfae2f5..0ccbe990 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", "certificateLevel": "QUALIFIED", diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json index 46af4e84..2f6ed5ce 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", "signatureProtocolParameters": { diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json index dbd2f3b5..4a0177cb 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", "certificateLevel": "QUALIFIED", diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json index 3a9c0349..683dbb84 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "QUALIFIED", "signatureProtocol": "ACSP_V2", diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json index 0f88439e..22276dab 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json @@ -1,4 +1,4 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO" } diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json index ab3d1b61..5fd36502 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "ACSP_V2", "signatureProtocolParameters": { diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json index 88c2a4d0..f1d018f4 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json index 834ac077..39c99d9e 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json @@ -1,4 +1,4 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json index cfff139b..b980efff 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "RAW_DIGEST_SIGNATURE", "signatureProtocolParameters": { diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json index e19b9623..00ceb1cb 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "RAW_DIGEST_SIGNATURE", "signatureProtocolParameters": { diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json index 38cfa286..16cf47ee 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "QUALIFIED", "initialCallbackUrl": "https://example.com/callback", diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json index c9a48b43..4c8d933b 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "QUALIFIED", "initialCallbackUrl": "https://example.com/callback" diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json index 88c2a4d0..f1d018f4 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json index 9aaf3ba7..7c17de75 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "certificateLevel": "QUALIFIED", "signatureProtocol": "RAW_DIGEST_SIGNATURE", diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json index e4cf80d4..0b2cc34d 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json @@ -1,5 +1,5 @@ { - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", "relyingPartyName": "DEMO", "signatureProtocol": "RAW_DIGEST_SIGNATURE", "signatureProtocolParameters": { diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json new file mode 100644 index 00000000..f44f8d16 --- /dev/null +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json @@ -0,0 +1,9 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "nonce": "cmFuZG9tTm9uY2U=", + "requestProperties": { + "shareMdClientIpAddress": true + } +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json new file mode 100644 index 00000000..5b1cac06 --- /dev/null +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json @@ -0,0 +1,4 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json new file mode 100644 index 00000000..c9b762c0 --- /dev/null +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json @@ -0,0 +1,3 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json new file mode 100644 index 00000000..39c99d9e --- /dev/null +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json @@ -0,0 +1,4 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/certificate-choice-session-request.json b/src/test/resources/requests/sign/notification/certificate-choice-session-request.json deleted file mode 100644 index 1bd3d1d8..00000000 --- a/src/test/resources/requests/sign/notification/certificate-choice-session-request.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "ADVANCED", - "nonce": "cmFuZG9tTm9uY2U=" -} \ No newline at end of file diff --git a/src/test/resources/responses/notification-certificate-choice-session-response.json b/src/test/resources/responses/notification-certificate-choice-session-response.json deleted file mode 100644 index 6b21efb5..00000000 --- a/src/test/resources/responses/notification-certificate-choice-session-response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" -} \ No newline at end of file diff --git a/src/test/resources/responses/session-status-account-unusable.json b/src/test/resources/responses/session-status-account-unusable.json new file mode 100644 index 00000000..9cf51132 --- /dev/null +++ b/src/test/resources/responses/session-status-account-unusable.json @@ -0,0 +1,8 @@ +{ + "state": "COMPLETE", + "result": { + "endResult": "ACCOUNT_UNUSABLE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json new file mode 100644 index 00000000..29b7df71 --- /dev/null +++ b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json @@ -0,0 +1,4 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} \ No newline at end of file From 29079d4cd7d02e31156bf5dd613f2e35439d4069 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Fri, 10 Oct 2025 10:48:54 +0300 Subject: [PATCH 53/57] Update notification based signature to use v3.1 endpoints (#141) * SLIB-126 - add util to create callbackUrl with url-token * SLIB-126 - move UrlSafeTokenGenerator and CallbackUrl to common package * SLIB-116 - update notification-based signature path and request object * SLIB-116 - update notification-based signature session request builder * SLIB-116 - add pattern validation for verification code value * SLIB-116 - convert NotificationCertificateChoiceSessionResponse into record * SLIB-116 - convert NotificationSignatureSessionResponse into record * SLIB-116 - convert VerificationCode into record * SLIB-116 - fix notification-based signature session readme integration tests * SLIB-116 - improve tests for notification-based signature flow * SLIB-116 - fix typo in package name * SLIB-116 - code style improvements * SLIB-116 - improve documentation for idempotent behaviour in Readme --- CHANGELOG.md | 3 + README.md | 53 +- ...iceLinkSignatureSessionRequestBuilder.java | 27 +- .../ee/sk/smartid/ErrorResultHandler.java | 2 +- ...dSignatureCertificatePurposeValidator.java | 2 +- ...ertificateChoiceSessionRequestBuilder.java | 2 +- ...icationSignatureSessionRequestBuilder.java | 121 ++-- ...enticationCertificatePurposeValidator.java | 4 +- ...enticationCertificatePurposeValidator.java | 2 +- ...nQualifiedSmartIdCertificateValidator.java | 2 +- ...tIdAuthenticationCertificateValidator.java | 2 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 11 +- .../sk/smartid/rest/SmartIdRestConnector.java | 11 +- .../DeviceLinkSignatureSessionRequest.java | 58 ++ ...cationCertificateChoiceSessionRequest.java | 6 +- ...ationCertificateChoiceSessionResponse.java | 23 +- .../rest/dao/NotificationInteraction.java | 25 - .../NotificationSignatureSessionRequest.java | 56 ++ .../NotificationSignatureSessionResponse.java | 35 +- .../RawDigestSignatureProtocolParameters.java | 7 + .../rest/dao/SignatureSessionRequest.java | 44 -- .../sk/smartid/rest/dao/VerificationCode.java | 35 +- ...inkSignatureSessionRequestBuilderTest.java | 88 +-- ...ficateChoiceSessionRequestBuilderTest.java | 7 +- ...ionSignatureSessionRequestBuilderTest.java | 522 +++++++++--------- .../java/ee/sk/smartid/SignableDataTest.java | 68 +++ .../java/ee/sk/smartid/SignableHashTest.java | 64 +++ .../java/ee/sk/smartid/SmartIdClientTest.java | 49 +- .../integration/ReadmeIntegrationTest.java | 118 +++- .../SmartIdRestIntegrationTest.java | 113 ++-- .../rest/SmartIdRestConnectorTest.java | 241 ++++---- ...otification-signature-session-request.json | 15 - ...-signature-session-request-all-fields.json | 18 + ...e-session-request-invalid-credentials.json | 13 + ...ion-signature-session-request-invalid.json | 3 + ...-session-request-only-required-fields.json | 13 + ...tification-signature-session-response.json | 7 + 37 files changed, 1088 insertions(+), 782 deletions(-) rename src/main/java/ee/sk/smartid/common/{certifiate => certificate}/NonQualifiedSmartIdCertificateValidator.java (98%) rename src/main/java/ee/sk/smartid/common/{certifiate => certificate}/SmartIdAuthenticationCertificateValidator.java (99%) create mode 100644 src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java create mode 100644 src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java delete mode 100644 src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java create mode 100644 src/test/java/ee/sk/smartid/SignableDataTest.java create mode 100644 src/test/java/ee/sk/smartid/SignableHashTest.java delete mode 100644 src/test/resources/requests/sign/notification/notification-signature-session-request.json create mode 100644 src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json create mode 100644 src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json create mode 100644 src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json create mode 100644 src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json create mode 100644 src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a39360de..02fbea75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. +## [3.1.17] - 2025-10-07 +- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint + ## [3.1.16] - 2025-10-04 - Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint - Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response diff --git a/README.md b/README.md index 04fe968f..5b3108cf 100644 --- a/README.md +++ b/README.md @@ -990,7 +990,7 @@ the Smart-ID API will stay waiting for the RP to start the [linked notification- * `relyingPartyUUID`: Required. UUID of the Relying Party. * `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. * `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotency. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotent behaviour. * `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. * `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. * `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. @@ -1218,9 +1218,6 @@ Jump to [Query session status](#example-of-using-session-status-poller-to-query- ### Notification-based signature session -> [!CAUTION] -> The notification-based signature has not yet been updated to be used with Smart-ID API v3.1 - #### Request Parameters The request parameters for the notification-based signature session are as follows: @@ -1230,21 +1227,23 @@ The request parameters for the notification-based signature session are as follo * `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. * `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values are `sha256WithRSAEncryption`, `sha384WithRSAEncryption`, `sha512WithRSAEncryption`. -* `allowedInteractionsOrder`: Required. An array of interaction objects defining the allowed interactions in order of preference. + * `signatureAlgorithm`: Required. Signature algorithm name. Only `rsassa-pss` is currently supported. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm used for digest. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded string of interactions to be used for a session. The interactions are defined in order of preference. * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `verificationCodeChoice`, `confirmationMessageAndVerificationCodeChoice`. + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. To be used for overriding idempotency. * `requestProperties`: requestProperties: * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. * `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. #### Response Parameters * `sessionID`: Required. String used to request the operation result. -* `verificationCode`: Required. Object describing the Verification Code to be displayed. - * `type`: Required. Type of the VC code. Currently, the only allowed type is `alphaNumeric4`. - * `value`: Required. Value of the VC code. +* `vc`: Required. Object describing the verification code details. + * `type`: Required. Type of the verification code. Currently, the only allowed type is `numeric4`. + * `value`: Required. Value of the verification code to be displayed to the user. #### Examples of initiating a notification-based signature session @@ -1262,21 +1261,20 @@ SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( ); // Build the notification signature request -NotificationSignatureSessionResponse signatureSessionResponse = client.createNotificationSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withAllowedInteractionsOrder(List.of( - NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the ")) // Display text should be concise and specific. + ) + .initSignatureSession(); -// Process the querying sessions status response +// Get the session ID and continue to querying session status String sessionID = signatureSessionResponse.sessionID(); // Display verification code to the user -String verificationCode = signatureSessionResponse.getVc().getValue(); +String verificationCode = signatureSessionResponse.vc().getValue(); ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -1300,11 +1298,11 @@ NotificationSignatureSessionResponse signatureResponse = client.createNotificati NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. .initSignatureSession(); -// Process the signature response +// Get the session ID and continue to querying session status String sessionID = signatureResponse.sessionID(); // Display verification code to the user -String verificationCode = signatureResponse.getVc().getValue(); +String verificationCode = signatureResponse.vc().getValue(); ``` Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. @@ -1339,7 +1337,12 @@ try { #### Using nonce to override idempotent behaviour -Authentication is used as an example, nonce can also be used with certificate choice and signature sessions requests by using method `withNonce("randomValue")`. +Idempotent behaviour means that if the session request with same values is made multiple times within a 15-second window, +the same response with identical values will be returned. If there is a need to override this behaviour, a nonce can be used. +Nonce value must be a random string with a minimum length of 1 and a maximum length of 30 characters. + +Notification-based signature request is used as an example. Nonce can also be used with other signing session request +(device-link signature and certificate choice; notification-based certificate choice) by using method `withNonce("randomValue")`. ```java NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 789ca28f..56687739 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -40,7 +40,7 @@ import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.util.InteractionUtil; import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; @@ -67,7 +67,7 @@ public class DeviceLinkSignatureSessionRequestBuilder { private String initialCallbackUrl; private DigestInput digestInput; - private SignatureSessionRequest signatureSessionRequest; + private DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest; /** * Constructs a new Smart-ID signature request builder with the given connector. @@ -251,29 +251,32 @@ public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String in */ public DeviceLinkSessionResponse initSignatureSession() { validateRequestParameters(); - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = createSignatureSessionRequest(); + DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(deviceLinkSignatureSessionRequest); validateResponseParameters(deviceLinkSignatureSessionResponse); - this.signatureSessionRequest = signatureSessionRequest; + this.deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequest; return deviceLinkSignatureSessionResponse; } /** - * Gets the SignatureSessionRequest that was used to initiate the signature session. + * Gets the DeviceLinkSignatureSessionRequest that was used to initiate the signature session. *

    * This method can only be called after {@link #initSignatureSession()} has been invoked. * * @return the signature request that was used to initiate the session * @throws SmartIdClientException if called before initSignatureSession() */ - public SignatureSessionRequest getSignatureSessionRequest() { - if (signatureSessionRequest == null) { + public DeviceLinkSignatureSessionRequest getSignatureSessionRequest() { + if (deviceLinkSignatureSessionRequest == null) { throw new SmartIdClientException("Signature session has not been initiated yet"); } - return signatureSessionRequest; + return deviceLinkSignatureSessionRequest; } - private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest request) { + private DeviceLinkSessionResponse initSignatureSession(DeviceLinkSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } if (!StringUtil.isEmpty(documentNumber)) { return connector.initDeviceLinkSignature(request, documentNumber); } else if (semanticsIdentifier != null) { @@ -283,11 +286,11 @@ private DeviceLinkSessionResponse initSignatureSession(SignatureSessionRequest r } } - private SignatureSessionRequest createSignatureSessionRequest() { + private DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), signatureAlgorithm.getAlgorithmName(), new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new SignatureSessionRequest(relyingPartyUUID, + return new DeviceLinkSignatureSessionRequest(relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index 875dedd0..26f323c1 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -58,7 +58,7 @@ public class ErrorResultHandler { * @throws SmartIdClientException when input parameter sessionResult is null * @throws UserActionException sub-exceptions based on end result * @throws UserAccountException sub-exceptions based on end result - * @throws ProtocolFailureException when there was an error in the process (e.g. shcema name is incorrect) + * @throws ProtocolFailureException when there was an error in the process (e.g. schema name is incorrect) * @throws ExpectedLinkedSessionException when different session type was started than expected * @throws SmartIdServerException when technical error occurred on server side * @throws UnprocessableSmartIdResponseException when unexpected end result was received diff --git a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java index 7498e7b0..52132b42 100644 --- a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java @@ -28,7 +28,7 @@ import java.security.cert.X509Certificate; -import ee.sk.smartid.common.certifiate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.util.CertificateAttributeUtil; diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index e5afc41c..f6b6aac3 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -187,7 +187,7 @@ private NotificationCertificateChoiceSessionRequest createCertificateChoiceReque } private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.getSessionID())) { + if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.sessionID())) { throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); } } diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 65ab044a..ead4a53b 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,17 +36,17 @@ import ee.sk.smartid.common.InteractionsMapper; import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** @@ -55,6 +56,8 @@ public class NotificationSignatureSessionRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[0-9]{4}$"); + private final SmartIdConnector connector; private String relyingPartyUUID; @@ -66,7 +69,7 @@ public class NotificationSignatureSessionRequestBuilder { private Set capabilities; private List interactions; private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; private DigestInput digestInput; /** @@ -150,20 +153,19 @@ public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { * @param capabilities the capabilities * @return this builder */ - public NotificationSignatureSessionRequestBuilder withCapabilities(Set capabilities) { - this.capabilities = capabilities; + public NotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); return this; } /** - * Sets the allowed interactions order. + * Sets the interactions. * - * @param allowedInteractionsOrder the allowed interactions order + * @param interactions the allowed interactions order * @return this builder */ - @Deprecated // TODO - 17.09.25: fix in SLIB-116 - public NotificationSignatureSessionRequestBuilder withAllowedInteractionsOrder(List allowedInteractionsOrder) { - this.interactions = allowedInteractionsOrder; + public NotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; return this; } @@ -193,12 +195,17 @@ public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(Signatu * Sets the data to be signed. *

    * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - * If both {@link SignableData} and {@link SignableHash} are provided, {@link SignableData} will take precedence. + *

    + * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. * * @param signableData the data to be signed * @return this builder instance + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableHash} */ public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash"); + } this.digestInput = signableData; return this; } @@ -208,11 +215,17 @@ public NotificationSignatureSessionRequestBuilder withSignableData(SignableData *

    * The provided {@link SignableHash} must contain a valid hash value and hash type, * which will be used as the digest in the signing request. + *

    + * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. * * @param signableHash the hash data to be signed * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableData} */ public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData"); + } this.digestInput = signableHash; return this; } @@ -226,33 +239,37 @@ public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash *

  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • *
* - * @return a {@link NotificationSignatureSessionResponse} containing session details such as - * session ID, session token, and session secret. + * @return a {@link NotificationSignatureSessionResponse} containing session details such as session ID and verification code + * @throws SmartIdRequestSetupException when the request parameters are not set correctly + * @throws UnprocessableSmartIdResponseException when the response from the Smart-ID service is invalid */ public NotificationSignatureSessionResponse initSignatureSession() { - validateParameters(); - SignatureSessionRequest signatureSessionRequest = createSignatureSessionRequest(); - NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(signatureSessionRequest); + validateRequestParameters(); + NotificationSignatureSessionRequest request = createSignatureSessionRequest(); + NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(request); validateResponseParameters(notificationSignatureSessionResponse); return notificationSignatureSessionResponse; } - private NotificationSignatureSessionResponse initSignatureSession(SignatureSessionRequest request) { + private NotificationSignatureSessionResponse initSignatureSession(NotificationSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } if (documentNumber != null) { return connector.initNotificationSignature(request, documentNumber); } else if (semanticsIdentifier != null) { return connector.initNotificationSignature(request, semanticsIdentifier); } else { - throw new IllegalArgumentException("Either documentNumber or semanticsIdentifier must be set."); + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); } } - private SignatureSessionRequest createSignatureSessionRequest() { + private NotificationSignatureSessionRequest createSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), signatureAlgorithm.getAlgorithmName(), new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new SignatureSessionRequest(relyingPartyUUID, + return new NotificationSignatureSessionRequest(relyingPartyUUID, relyingPartyName, certificateLevel != null ? certificateLevel.name() : null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), @@ -260,28 +277,32 @@ private SignatureSessionRequest createSignatureSessionRequest() { nonce, capabilities, InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - null + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null ); } - private void validateParameters() { - if (relyingPartyUUID == null || relyingPartyUUID.isEmpty()) { - throw new SmartIdClientException("Relying Party UUID must be set."); + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); } - if (relyingPartyName == null || relyingPartyName.isEmpty()) { - throw new SmartIdClientException("Relying Party Name must be set."); + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); } - validateAllowedInteractions(); - - if (nonce != null && (nonce.length() < 1 || nonce.length() > 30)) { - throw new SmartIdClientException("Nonce length must be between 1 and 30 characters."); + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } + validateInteractions(); + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); } } - private void validateAllowedInteractions() { + private void validateInteractions() { if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdClientException("Allowed interactions order must be set and contain at least one interaction."); + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); } if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); @@ -289,31 +310,29 @@ private void validateAllowedInteractions() { } private void validateResponseParameters(NotificationSignatureSessionResponse response) { - if (StringUtil.isEmpty(response.getSessionID())) { - logger.error("Session ID is missing from the response"); - throw new UnprocessableSmartIdResponseException("Session ID is missing from the response"); + if (StringUtil.isEmpty(response.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'sessionID' is missing or empty"); } - VerificationCode verificationCode = response.getVc(); + VerificationCode verificationCode = response.vc(); if (verificationCode == null) { - logger.error("VC object is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC object is missing from the response"); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc' is missing"); } - - String vcType = verificationCode.getType(); + String vcType = verificationCode.type(); if (StringUtil.isEmpty(vcType)) { - logger.error("VC type is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC type is missing from the response"); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' is missing or empty"); } - - if (!VerificationCode.ALPHA_NUMERIC_4.equals(vcType)) { - logger.error("Unsupported VC type: {}", vcType); - throw new UnprocessableSmartIdResponseException("Unsupported VC type: " + vcType); + if (!VerificationCodeType.NUMERIC4.getValue().equals(vcType)) { + logger.error("Notification-based signature response field 'vc.type' contains unsupported value '{}'", vcType); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' contains unsupported value"); } - - if (StringUtil.isEmpty(verificationCode.getValue())) { - logger.error("VC value is missing from the response"); - throw new UnprocessableSmartIdResponseException("VC value is missing from the response"); + if (StringUtil.isEmpty(verificationCode.value())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' is missing or empty"); + } + if (!VERIFICATION_CODE_PATTERN.matcher(verificationCode.value()).matches()) { + logger.error("Notification-based signature response field 'vc.value' does not match the required pattern. Expected pattern: {}; actual value: {}", + VERIFICATION_CODE_PATTERN.pattern(), verificationCode.value()); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' does not match the required pattern"); } } } diff --git a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java index dcd1e414..41a03d63 100644 --- a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java @@ -28,8 +28,8 @@ import java.security.cert.X509Certificate; -import ee.sk.smartid.common.certifiate.NonQualifiedSmartIdCertificateValidator; -import ee.sk.smartid.common.certifiate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; import ee.sk.smartid.exception.permanent.SmartIdClientException; /** diff --git a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java index ca62af1a..1bcc7150 100644 --- a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java @@ -32,7 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ee.sk.smartid.common.certifiate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; import ee.sk.smartid.util.CertificateAttributeUtil; diff --git a/src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java similarity index 98% rename from src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java rename to src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java index 2e5f85a8..5dbf7389 100644 --- a/src/main/java/ee/sk/smartid/common/certifiate/NonQualifiedSmartIdCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.common.certifiate; +package ee.sk.smartid.common.certificate; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java similarity index 99% rename from src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java rename to src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java index f23ebf89..716045ab 100644 --- a/src/main/java/ee/sk/smartid/common/certifiate/SmartIdAuthenticationCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java @@ -1,4 +1,4 @@ -package ee.sk.smartid.common.certifiate; +package ee.sk.smartid.common.certificate; /*- * #%L diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 819184eb..7d4fb503 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -43,10 +43,11 @@ import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; public interface SmartIdConnector extends Serializable { @@ -108,7 +109,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a device link based signature sessions. @@ -117,7 +118,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. */ - DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, String documentNumber); + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber); /** * Initiates a notification-based signature session using a semantics identifier. @@ -126,7 +127,7 @@ public interface SmartIdConnector extends Serializable { * @param semanticsIdentifier The semantics identifier for the user initiating the session. * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. */ - NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); /** * Initiates a notification-based signature session using a document number. @@ -135,7 +136,7 @@ public interface SmartIdConnector extends Serializable { * @param documentNumber The document number for the user initiating the session. * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. */ - NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, String documentNumber); + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber); /** * Set the SSL context to use for secure communication diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 4ecbdbb2..1cf64e6c 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -55,11 +55,12 @@ import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SessionStatusRequest; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.ForbiddenException; @@ -227,7 +228,7 @@ public CertificateResponse getCertificateByDocumentNumber(String documentNumber, } @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -237,7 +238,7 @@ public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest } @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest request, String documentNumber) { + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) @@ -247,7 +248,7 @@ public DeviceLinkSessionResponse initDeviceLinkSignature(SignatureSessionRequest } @Override - public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) @@ -257,7 +258,7 @@ public NotificationSignatureSessionResponse initNotificationSignature(SignatureS } @Override - public NotificationSignatureSessionResponse initNotificationSignature(SignatureSessionRequest request, String documentNumber) { + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber) { URI uri = UriBuilder .fromUri(endpointUrl) .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java new file mode 100644 index 00000000..e1f16fa2 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java @@ -0,0 +1,58 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Device link-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java index f4c67f6c..a665abde 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,6 +26,7 @@ * #L% */ + import java.io.Serializable; import java.util.Set; @@ -48,5 +49,4 @@ public record NotificationCertificateChoiceSessionRequest( @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { - -} \ No newline at end of file +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java index e4b60d87..77e943ea 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,16 +30,11 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Notification-based certificate choice response + * + * @param sessionID Required. The ID of the created certificate choice session. + */ @JsonIgnoreProperties(ignoreUnknown = true) -public class NotificationCertificateChoiceSessionResponse implements Serializable { - - private String sessionID; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } +public record NotificationCertificateChoiceSessionResponse(String sessionID) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java deleted file mode 100644 index b5fe3eaf..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationInteraction.java +++ /dev/null @@ -1,25 +0,0 @@ -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java new file mode 100644 index 00000000..7abdd7e9 --- /dev/null +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java @@ -0,0 +1,56 @@ +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + */ +public record NotificationSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { +} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java index 420c827b..9085d2d1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,26 +30,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -@JsonIgnoreProperties(ignoreUnknown = true) -public class NotificationSignatureSessionResponse implements Serializable { - - private String sessionID; - - private VerificationCode vc; - - public String getSessionID() { - return sessionID; - } - - public void setSessionID(String sessionID) { - this.sessionID = sessionID; - } - - public VerificationCode getVc() { - return vc; - } +/** + * Notification-based signature session request + * + * @param sessionID Required. The ID of the created signature session. + * @param vc Required. Verification code details + */ - public void setVc(VerificationCode verificationCode) { - this.vc = verificationCode; - } +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationSignatureSessionResponse(String sessionID, VerificationCode vc) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index e9591a73..16e9fe9b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -28,6 +28,13 @@ import java.io.Serializable; +/** + * Parameters for protocol RAW_DIGEST_SIGNATURE + * + * @param digest Required. The digest to be signed, Base64 encoded. + * @param signatureAlgorithm Required. The signature algorithm. Supported value is RSASSA-PSS. + * @param signatureAlgorithmParameters Required. The parameters for signature algorithm. + */ public record RawDigestSignatureProtocolParameters(String digest, String signatureAlgorithm, SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java deleted file mode 100644 index 7f40fce3..00000000 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureSessionRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -public record SignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { -} \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index b7580803..db9ff91d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,29 +30,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Verification code details + * + * @param type Required. Verification code type + * @param value Required. Verification code value + */ @JsonIgnoreProperties(ignoreUnknown = true) -public class VerificationCode implements Serializable { - - @Deprecated // TODO - 16.09.25: will be removed with notification-based signature flow changes; SLIB-116 - public static final String ALPHA_NUMERIC_4 = "alphaNumeric4"; - - private String type; - - private String value; - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } +public record VerificationCode(String type, String value) implements Serializable { } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 8f156e53..4d1cb1e2 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -64,7 +64,7 @@ import ee.sk.smartid.rest.SmartIdConnector; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; class DeviceLinkSignatureSessionRequestBuilderTest { @@ -79,7 +79,7 @@ void setUp() { @Test void initSignatureSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); @@ -94,8 +94,10 @@ void initSignatureSession_withSemanticsIdentifier() { @Test void initSignatureSession_withDocumentNumber() { String documentNumber = "PNOEE-31111111111-MOCK-Q"; - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b + .withSemanticsIdentifier(null) + .withDocumentNumber(documentNumber)); DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); @@ -109,16 +111,16 @@ void initSignatureSession_withDocumentNumber() { @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest request = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.certificateLevel()); } @@ -126,49 +128,49 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) void initSignatureSession_withNonce_ok(String nonce) { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest request = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.nonce()); } @Test void initSignatureSession_withRequestProperties() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertNotNull(capturedRequest.requestProperties()); assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); } @Test void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @@ -176,7 +178,7 @@ void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { @ParameterizedTest @EnumSource(HashAlgorithm.class) void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); @@ -184,9 +186,9 @@ void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); } @@ -194,7 +196,7 @@ void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { @ParameterizedTest @EnumSource(HashAlgorithm.class) void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); @@ -202,9 +204,9 @@ void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { assertNotNull(signatureSessionResponse); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); @@ -215,69 +217,69 @@ void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { @ValueSource(strings = {" "}) void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))) + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))) .thenReturn(mockSignatureSessionResponse()); DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); assertEquals("test-session-id", response.sessionID()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest request = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); assertEquals(0, request.capabilities().size()); } @ParameterizedTest @ArgumentsSource(CapabilitiesArgumentProvider.class) void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(expectedCapabilities, capturedRequest.capabilities()); } @Test void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); assertNotNull(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @Test void getSignatureSessionRequest_ok() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - SignatureSessionRequest signatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); assertNotNull(signature); - assertEquals("test-relying-party-uuid", signatureSessionRequest.relyingPartyUUID()); - assertEquals("DEMO", signatureSessionRequest.relyingPartyName()); - assertEquals("RAW_DIGEST_SIGNATURE", signatureSessionRequest.signatureProtocol()); - assertNotNull(signatureSessionRequest.signatureProtocolParameters()); - assertNotNull(signatureSessionRequest.interactions()); + assertEquals("test-relying-party-uuid", deviceLinkSignatureSessionRequest.relyingPartyUUID()); + assertEquals("DEMO", deviceLinkSignatureSessionRequest.relyingPartyName()); + assertEquals("RAW_DIGEST_SIGNATURE", deviceLinkSignatureSessionRequest.signatureProtocol()); + assertNotNull(deviceLinkSignatureSessionRequest.signatureProtocolParameters()); + assertNotNull(deviceLinkSignatureSessionRequest.interactions()); } @Test void getSignatureSessionRequest_sessionNotStarted_throwException() { - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); @@ -419,7 +421,7 @@ void validateResponseParameters_missingSessionID(String sessionID) { "test-session-secret", URI.create("https://example.com/device-link")); var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); @@ -433,7 +435,7 @@ void validateResponseParameters_missingSessionToken(String sessionToken) { "test-session-secret", URI.create("https://example.com/device-link")); var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); @@ -447,7 +449,7 @@ void validateResponseParameters_missingSessionSecret(String sessionSecret) { sessionSecret, URI.create("https://example.com/device-link")); var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); @@ -461,7 +463,7 @@ void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String d "test-session-secret", deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 797f59dd..9fc5f516 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -216,8 +216,7 @@ class ValidateRequiredResponseParameters { @ParameterizedTest @NullAndEmptySource void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); - notificationCertificateChoiceSessionResponse.setSessionID(sessionId); + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(sessionId); when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); @@ -227,9 +226,7 @@ void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException } private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { - var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(); - notificationCertificateChoiceSessionResponse.setSessionID(RELYING_PARTY_UUID); - return notificationCertificateChoiceSessionResponse; + return new NotificationCertificateChoiceSessionResponse("00000000-0000-0000-0000-000000000000"); } private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index 2def28ae..f2731d5d 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -29,7 +29,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -37,12 +36,13 @@ import static org.mockito.Mockito.when; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -53,88 +53,73 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import ee.sk.smartid.common.notification.interactions.NotificationInteraction; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.rest.dao.VerificationCode; -@Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") class NotificationSignatureSessionRequestBuilderTest { + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111"; + private SmartIdConnector connector; - private NotificationSignatureSessionRequestBuilder builder; @BeforeEach void setUp() { connector = mock(SmartIdConnector.class); - - builder = new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withAllowedInteractionsOrder(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableData(new SignableData("Test data".getBytes())); } @Test - void initSignatureSession_withSemanticsIdentifier() { - var semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - builder.withSemanticsIdentifier(semanticsIdentifier); + void initSignatureSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockNotificationSignatureSessionResponse()); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), eq(semanticsIdentifier))).thenReturn(mockNotificationSignatureSessionResponse()); + NotificationSignatureSessionResponse signature = toBaseNotificationSignatureSessionRequestBuilder().initSignatureSession(); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - - assertNotNull(signature); - assertEquals("test-session-id", signature.getSessionID()); - assertEquals("alphaNumeric4", signature.getVc().getType()); - assertEquals("4927", signature.getVc().getValue()); + assertSessionResponse(signature); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(semanticsIdentifier)); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(SEMANTICS_IDENTIFIER)); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @Test - void initSignatureSession_withDocumentNumber() { - String documentNumber = "PNOEE-31111111111"; - builder.withDocumentNumber(documentNumber); + void initSignatureSession_withDocumentNumber_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(mockNotificationSignatureSessionResponse()); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockNotificationSignatureSessionResponse()); + NotificationSignatureSessionResponse signature = toNotificationSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(null).withDocumentNumber(DOCUMENT_NUMBER)) + .initSignatureSession(); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + assertSessionResponse(signature); - assertNotNull(signature); - assertEquals("test-session-id", signature.getSessionID()); - assertEquals("alphaNumeric4", signature.getVc().getType()); - assertEquals("4927", signature.getVc().getValue()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(documentNumber)); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); } @ParameterizedTest @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { - builder.withCertificateLevel(certificateLevel).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_withCertificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + toNotificationSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) + .initSignatureSession(); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest request = requestCaptor.getValue(); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); assertEquals(expectedValue, request.certificateLevel()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); @@ -143,337 +128,352 @@ void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel @ParameterizedTest @ArgumentsSource(ValidNonceArgumentSourceProvider.class) void initSignatureSession_withNonce_ok(String nonce) { - builder.withNonce(nonce).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - - assertNotNull(signature); + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)) + .initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest request = requestCaptor.getValue(); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); assertEquals(nonce, request.nonce()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); } - @Test - void withSignatureAlgorithm_setsCorrectAlgorithm() { - var signableData = new SignableData("Test data".getBytes()); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initSignatureSession_withRequestProperties_ok(boolean shareIp) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockNotificationSignatureSessionResponse()); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); + toNotificationSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(shareIp)) + .initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.requestProperties()); + assertEquals(shareIp, capturedRequest.requestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } @Test - void initSignatureSession_withRequestProperties() { - builder.withShareMdClientIpAddress(true).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_useDefaultHashAlgorithmForSignableHash_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableHash = new SignableHash("Test data".getBytes()); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.requestProperties()); - assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } - @Disabled("Signature algorithm has changed") @ParameterizedTest @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { + void initSignatureSession_overrideDefaultHashAlgorithmForSignableHash_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); - builder.withSignableData(null).withSignableHash(signableHash).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(hashAlgorithm.getAlgorithmName().toLowerCase() + "WithRSAEncryption", capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities(Set capabilities, Set expectedCapabilities) { - builder.withCapabilities(capabilities).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); + @Test + void initSignatureSession_useDefaultHashAlgorithmForSignableData_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableData = new SignableData("Test data".getBytes()); - assertNotNull(signature); + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.capabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); } @ParameterizedTest @EnumSource(HashAlgorithm.class) - void initSignatureSession_withHashType_overridesExplicitSignatureAlgorithm(HashAlgorithm hashAlgorithm) { + void initSignatureSession_overrideDefaultHashAlgorithmForSignableData_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); - builder.withSignableData(signableData).withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } - @Test - void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { - var signableData = new SignableData("Test data".getBytes()); - builder.withSignableData(signableData).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); + toNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)) + .initSignatureSession(); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } - @Test - void getSignatureAlgorithm_withDefaultAlgorithmWhenNoSignableDataOrHash() { - builder.withSignableData(null).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = builder.initSignatureSession(); - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(SignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - SignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } @Nested class ErrorCases { - @Test - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier() { - builder.withDocumentNumber(null).withSemanticsIdentifier(null); + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyUUID_throwException(String relyingPartyUUID) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - var ex = assertThrows(IllegalArgumentException.class, () -> builder.initSignatureSession()); - assertEquals("Either documentNumber or semanticsIdentifier must be set.", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); } - @Test - void initSignatureSession_whenSignableDataHashTypeIsNull() { - SignableData signableData = new SignableData("Test data".getBytes()); - builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyName_throwException(String relyingPartyName) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - SmartIdClientException exception = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("HashType must be set for signableData.", exception.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); } @Test - void initSignatureSession_whenHashTypeIsNull() { - var signableData = new SignableData("Test data".getBytes()); - builder.withSignableData(signableData).withSignableHash(null).withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_semanticIdentifierAndDocumentNumberAreBothSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(DOCUMENT_NUMBER).withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("HashType must be set for signableData.", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", ex.getMessage()); } - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_whenAllowedInteractionsOrderIsNullOrEmpty(List allowedInteractionsOrder) { - builder.withAllowedInteractionsOrder(allowedInteractionsOrder); + @Test + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Allowed interactions order must be set and contain at least one interaction.", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", ex.getMessage()); } - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyUUID(String relyingPartyUUID) { - builder.withRelyingPartyUUID(relyingPartyUUID); + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Relying Party UUID must be set.", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); } - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyName(String relyingPartyName) { - builder.withRelyingPartyName(relyingPartyName); + @Test + void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Relying Party Name must be set.", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); } @Test - void initSignatureSession_invalidNonce() { - builder.withNonce("1234567890123456789012345678901"); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableData", ex.getMessage()); } @Test - void initSignatureSession_emptyNonce() { - builder.withNonce(""); - var ex = assertThrows(SmartIdClientException.class, () -> builder.initSignatureSession()); - assertEquals("Nonce length must be between 1 and 30 characters.", ex.getMessage()); + void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableHash", ex.getMessage()); } - } - - @Nested - class ResponseValidationTests { @ParameterizedTest @NullAndEmptySource - void validateResponse_missingSessionID(String sessionID) { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void initSignatureSession_interactionsAreNotProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID(sessionID); - response.setVc(new VerificationCode()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + @Test + void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("Session ID is missing from the response", ex.getMessage()); + var exception = assertThrows(SmartIdClientException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); } @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingVerificationCodeType(String vcType) { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initSignatureSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); - - VerificationCode verificationCode = new VerificationCode(); - verificationCode.setType(vcType); - response.setVc(verificationCode); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("VC type is missing from the response", ex.getMessage()); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); } - @Test - void validateResponse_unsupportedVerificationCodeType() { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters", ex.getMessage()); + } + } - VerificationCode vc = new VerificationCode(); - vc.setType("unsupportedType"); - response.setVc(vc); + @Nested + class ResponseValidationTests { - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingSessionID_throwException(String sessionID) { + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(sessionID, new VerificationCode(null, null)); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("Unsupported VC type: unsupportedType", ex.getMessage()); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'sessionID' is missing or empty", ex.getMessage()); } @ParameterizedTest @NullSource - void validateResponseParameters_missingVerificationCodeObject(VerificationCode vc) { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); + void validateResponseParameters_missingVerificationCode_throwException(VerificationCode verificationCode) { + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); - response.setVc(vc); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc' is missing", ex.getMessage()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("VC object is missing from the response", ex.getMessage()); + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeType_throwException(String vcType) { + var verificationCode = new VerificationCode(vcType, null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' is missing or empty", ex.getMessage()); } @Test - void validateResponseParameters_emptyVerificationCode() { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); - - VerificationCode emptyVc = new VerificationCode(); - response.setVc(emptyVc); - - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("VC type is missing from the response", ex.getMessage()); + void validateResponse_unsupportedVerificationCodeType_throwException() { + var verificationCode = new VerificationCode("unsupportedType", null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' contains unsupported value", ex.getMessage()); } @ParameterizedTest @NullAndEmptySource - void validateResponse_missingVerificationCodeValue(String vcValue) { - builder.withSemanticsIdentifier(new SemanticsIdentifier("PNO", "EE", "31111111111")); - - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); + void validateResponse_missingVerificationCodeValue_throwException(String vcValue) { + var verificationCode = new VerificationCode("numeric4", vcValue); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' is missing or empty", ex.getMessage()); + } - VerificationCode vc = new VerificationCode(); - vc.setType("alphaNumeric4"); - vc.setValue(vcValue); - response.setVc(vc); + @Test + void validateResponse_verificationCodeDoesNotMatchPattern_throwException() { + var verificationCode = new VerificationCode("numeric4", "aaaaaa"); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' does not match the required pattern", ex.getMessage()); + } + } - when(connector.initNotificationSignature(any(SignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + private NotificationSignatureSessionRequestBuilder toNotificationSignatureSessionRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationSignatureSessionRequestBuilder()); + } - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builder.initSignatureSession()); - assertEquals("VC value is missing from the response", ex.getMessage()); - } + private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSessionRequestBuilder() { + return new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); } private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { - var response = new NotificationSignatureSessionResponse(); - response.setSessionID("test-session-id"); + var verificationCode = new VerificationCode("numeric4","4927"); + return toNotificationSignatureSessionResponse(verificationCode); + } - var vc = new VerificationCode(); - vc.setType("alphaNumeric4"); - vc.setValue("4927"); - response.setVc(vc); + private static NotificationSignatureSessionResponse toNotificationSignatureSessionResponse(VerificationCode verificationCode) { + return new NotificationSignatureSessionResponse("00000000-0000-0000-0000-000000000000", verificationCode); + } - return response; + private static void assertSessionResponse(NotificationSignatureSessionResponse signature) { + assertNotNull(signature); + assertEquals("00000000-0000-0000-0000-000000000000", signature.sessionID()); + assertEquals("numeric4", signature.vc().type()); + assertEquals("4927", signature.vc().value()); } private static class CertificateLevelArgumentProvider implements ArgumentsProvider { @@ -482,18 +482,8 @@ public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of(null, null), Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Set.of("QUALIFIED", "ADVANCED"), Set.of("QUALIFIED", "ADVANCED")), - Arguments.of(Set.of("QUALIFIED"), Set.of("QUALIFIED")), - Arguments.of(Set.of(), Set.of()) + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") ); } } diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java new file mode 100644 index 00000000..82d57a2d --- /dev/null +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -0,0 +1,68 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableDataTest { + + private static final byte[] TEST_DATA = "Test data".getBytes(); + + @Test + void getDigestInBase64() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertEquals(Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512)), signableData.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableData.hashAlgorithm()); + } + + @Test + void calculateHash() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertArrayEquals(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512), signableData.calculateHash()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashProvided_throwException(byte[] dataToSign) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(dataToSign)); + assertEquals("Parameter 'dataToSign' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmSetToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(TEST_DATA, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/SignableHashTest.java new file mode 100644 index 00000000..7d6bdf7c --- /dev/null +++ b/src/test/java/ee/sk/smartid/SignableHashTest.java @@ -0,0 +1,64 @@ +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableHashTest { + + private static final byte[] DIGEST = DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512); + + @Test + void getDigestInBase64() { + SignableHash signableHash = new SignableHash(DIGEST, HashAlgorithm.SHA_512); + + assertEquals(Base64.getEncoder().encodeToString(DIGEST), signableHash.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableHash.hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashValueProvided_throwException(byte[] hash) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(hash)); + assertEquals("Parameter 'hash' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmOverriddenToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(DIGEST, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 13681fc5..a44def42 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -39,7 +39,6 @@ import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -51,13 +50,14 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.VerificationCode; class SmartIdClientTest { @@ -151,7 +151,7 @@ void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredF .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) .initCertificateChoice(); - assertNotNull(response.getSessionID()); + assertNotNull(response.sessionID()); } @Test @@ -167,7 +167,7 @@ void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok( .withShareMdClientIpAddress(true) .initCertificateChoice(); - assertNotNull(response.getSessionID()); + assertNotNull(response.sessionID()); } } @@ -395,7 +395,6 @@ void createNotificationAuthentication_withDocumentNumber() { } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18089) class NotificationBasedSignatureSession { @@ -403,43 +402,41 @@ class NotificationBasedSignatureSession { @Test void createNotificationSignature_withDocumentNumber() { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/notification/notification-signature-session-request.json", - "responses/notification-session-response.json"); + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); var signableHash = new SignableHash("a".repeat(64).getBytes()); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() .withDocumentNumber(DOCUMENT_NUMBER) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.confirmationMessage("Verify the code"))) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) .withSignableHash(signableHash) .initSignatureSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertSessionResponse(response); } @Test void createNotificationSignature_withSemanticsIdentifier() { SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/notification-signature-session-request.json", - "responses/notification-session-response.json"); + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); var signableHash = new SignableHash("a".repeat(64).getBytes()); NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withAllowedInteractionsOrder(List.of(NotificationInteraction.confirmationMessage("Verify the code"))) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) .withSignableHash(signableHash) .initSignatureSession(); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertSessionResponse(response); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); } } @@ -632,7 +629,7 @@ void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { .withSignableHash(signableHash) .withInitialCallbackUrl(INITIAL_CALLBACK_URL); DeviceLinkSessionResponse response = builder.initSignatureSession(); - SignatureSessionRequest request = builder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); URI deviceLink = smartIdClient.createDynamicContent() .withSchemeName("smart-id-demo") @@ -662,7 +659,7 @@ void createDynamicContent_withQrCode() { .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash); DeviceLinkSessionResponse response = builder.initSignatureSession(); - SignatureSessionRequest request = builder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); @@ -694,7 +691,7 @@ void createDynamicContent_withQrCodeImage() { .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) .withSignableHash(signableHash); DeviceLinkSessionResponse response = builder.initSignatureSession(); - SignatureSessionRequest request = builder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); URI qrCodeUri = smartIdClient.createDynamicContent() diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index 07c05060..b3619a6f 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -72,6 +72,9 @@ import ee.sk.smartid.RpChallengeGenerator; import ee.sk.smartid.SessionType; import ee.sk.smartid.SignableData; +import ee.sk.smartid.SignatureCertificatePurposeValidator; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactory; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactoryImpl; import ee.sk.smartid.SignatureResponse; import ee.sk.smartid.SignatureResponseValidator; import ee.sk.smartid.SignatureValueValidator; @@ -86,6 +89,7 @@ import ee.sk.smartid.rest.SessionStatusPoller; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; @@ -93,13 +97,12 @@ import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.CallbackUrlUtil; @SmartIdDemoIntegrationTest public class ReadmeIntegrationTest { - private static final String ALPHA_NUMERIC_PATTERN = "^[A-z0-9]{4}$"; + private static final Pattern NUMERIC_PATTERN = Pattern.compile("^[0-9]{4}$"); private SmartIdClient smartIdClient; @@ -383,7 +386,7 @@ void signature_withDocumentNumberAndQRCode() { .withInteractions(signatureInteractions); DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); // Get SignatureSessionRequest after the request is made and store for validations - SignatureSessionRequest signatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); // Process the signature response String signatureSessionId = signatureSessionResponse.sessionID(); @@ -408,7 +411,7 @@ void signature_withDocumentNumberAndQRCode() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") - .withInteractions(signatureSessionRequest.interactions()) + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) .buildDeviceLink(sessionSecret); // Return URI to be used with QR-code generation library on the frontend side @@ -454,7 +457,7 @@ void signature_withSemanticIdentifier() { .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); - String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); // SessionID is used to query sessions status later // Get the session status poller @@ -491,7 +494,7 @@ void signature_withSemanticIdentifier() { // Init signature session DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); // Get SignatureSessionRequest after the request is made and store for validations - SignatureSessionRequest signatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); // Process the signature response String signatureSessionId = signatureSessionResponse.sessionID(); @@ -515,7 +518,7 @@ void signature_withSemanticIdentifier() { .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) .withElapsedSeconds(elapsedSeconds) .withLang("est") - .withInteractions(signatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request .buildDeviceLink(sessionSecret); // Display QR-code to the user @@ -670,7 +673,7 @@ void certificateChoice_withSemanticIdentifier() { .withCertificateLevel(requestedCertificateLevel) .initCertificateChoice(); - String sessionId = certificateChoiceSessionResponse.getSessionID(); + String sessionId = certificateChoiceSessionResponse.sessionID(); // SessionID is used to query sessions status later // Get the session status poller @@ -690,7 +693,6 @@ void certificateChoice_withSemanticIdentifier() { assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); } - @Disabled("Not yet updated to work with v3.1") @Test void signature_withSemanticsIdentifier() { var semanticIdentifier = new SemanticsIdentifier( @@ -700,14 +702,15 @@ void signature_withSemanticsIdentifier() { SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code "40504040001"); // identifier (according to country and identity type reference) + CertificateLevel certificateLevel = CertificateLevel.QSCD; NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient .createNotificationCertificateChoice() .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .withCertificateLevel(certificateLevel) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" .initCertificateChoice(); - String certificateChoiceSessionId = certificateChoiceSessionResponse.getSessionID(); // SessionID is used to query sessions status later + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); // Get the session status poller SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); @@ -718,7 +721,7 @@ void signature_withSemanticsIdentifier() { TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus, certificateLevel); // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); // Create the signable data @@ -732,23 +735,20 @@ void signature_withSemanticsIdentifier() { ); NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) + .withCertificateLevel(certificateLevel) .withSignableData(signableData) .withSemanticsIdentifier(semanticsIdentifier) - .withAllowedInteractionsOrder(List.of( + .withInteractions(List.of( NotificationInteraction.confirmationMessage("Please sign the document")) ) - .withNonce("random") .initSignatureSession(); - // Process the querying sessions status response - String sessionID = signatureSessionResponse.getSessionID(); + // Get the session ID and continue to querying session status + String sessionID = signatureSessionResponse.sessionID(); // Display verification code to the user - String verificationCode = signatureSessionResponse.getVc().getValue(); - assertTrue(Pattern.matches(ALPHA_NUMERIC_PATTERN, verificationCode)); + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); // Get sessionID from current session response and poll for session status SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); @@ -756,13 +756,76 @@ void signature_withSemanticsIdentifier() { assertEquals("COMPLETE", signatureSessionStatus.getState()); SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOEE-40504040001-DEMO-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withDocumentNumber() { + String documentNumber = "PNOEE-40504040001-DEMO-Q"; + + CertificateLevel certificateLevel = CertificateLevel.QSCD; + // Query the certificate by document number to be used for creating the DataToSign + CertificateByDocumentNumberResult certificateByDocumentNumber = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .withCertificateLevel(certificateLevel) + .getCertificateByDocumentNumber(); + + // Set up the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + // Validate the certificate is trusted and active + certificateValidator.validate(certificateByDocumentNumber.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateByDocumentNumber.certificateLevel()); + certificatePurposeValidator.validate(certificateByDocumentNumber.certificate()); + + // For example use digidoc4j with SignatureBuilder to create DataToSign using `certificateByDocumentNumber.certificate()` + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(certificateLevel) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the document")) + ) + .initSignatureSession(); + + // Get the session ID and continue to querying session status + String signatureSessionId = signatureSessionResponse.sessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(documentNumber, signatureResponse.getDocumentNumber()); assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); - assertEquals("verificationCodeChoice", signatureResponse.getInteractionFlowUsed()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); assertNotNull(signatureResponse.getCertificate()); } } @@ -786,6 +849,11 @@ void queryCertificate() { // Validate the certificate certificateValidator.validate(certResponse.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certResponse.certificateLevel()); + certificatePurposeValidator.validate(certResponse.certificate()); } } diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 317c97b0..69ba46f4 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -53,30 +53,30 @@ import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; import ee.sk.smartid.util.InteractionUtil; @SmartIdDemoIntegrationTest class SmartIdRestIntegrationTest { - // Replace these to test with V3 private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String UUID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; - private static final String VERIFICATION_CODE_PATTERN = "^[A-Za-z0-9]{4}$"; - private static final String SESSION_TOKEN_PATTERN = "^[A-Za-z0-9]{24}$"; - private static final String SESSION_SECRET_PATTERN = "^[A-Za-z0-9+/]{24}$"; + private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9]{4}$"); + private static final Pattern SESSION_TOKEN_PATTERN = Pattern.compile("^[A-Za-z0-9]{24}$"); + private static final Pattern SESSION_SECRET_PATTERN = Pattern.compile("^[A-Za-z0-9+/]{24}$"); private SmartIdConnector smartIdConnector; @@ -85,7 +85,7 @@ void setUp() { smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); } - @Disabled("Demo accounts for device link requests not yet available") + @Disabled("Testing device-link flows with demo accounts is not yet possible") @Nested class DeviceLink { @@ -96,24 +96,24 @@ class Authentication { void initAnonymousDeviceLinkAuthentication() { DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); - assertNotNull(sessionsResponse.receivedAt()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); } @Test void initDeviceLinkAuthentication_withDocumentNumber() { DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); - assertNotNull(sessionsResponse.receivedAt()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); } @Test @@ -122,9 +122,9 @@ void initDeviceLinkAuthentication_withSemanticsIdentifier() { DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionResponse.sessionSecret())); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); assertNotNull(sessionResponse.receivedAt()); } @@ -161,13 +161,13 @@ void initDeviceLinkCertificateChoice() { null ); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); - assertNotNull(sessionsResponse.deviceLinkBase()); - assertNotNull(sessionsResponse.receivedAt()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.deviceLinkBase()); + assertNotNull(sessionResponse.receivedAt()); } } @@ -179,7 +179,7 @@ void initDeviceLinkSignature_withSemanticIdentifier() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new SignatureSessionRequest(RELYING_PARTY_UUID, + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), @@ -191,12 +191,12 @@ void initDeviceLinkSignature_withSemanticIdentifier() { null ); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); - assertNotNull(sessionsResponse.receivedAt()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); } @Test @@ -205,7 +205,7 @@ void initDeviceLinkSignature_withDocumentNumber() { Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new SignatureSessionRequest(RELYING_PARTY_UUID, + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), @@ -217,12 +217,12 @@ void initDeviceLinkSignature_withDocumentNumber() { null ); - DeviceLinkSessionResponse sessionsResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); - assertTrue(Pattern.matches(UUID_PATTERN, sessionsResponse.sessionID())); - assertTrue(Pattern.matches(SESSION_TOKEN_PATTERN, sessionsResponse.sessionToken())); - assertTrue(Pattern.matches(SESSION_SECRET_PATTERN, sessionsResponse.sessionSecret())); - assertNotNull(sessionsResponse.receivedAt()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); } } } @@ -230,6 +230,9 @@ void initDeviceLinkSignature_withDocumentNumber() { @Nested class NotificationBasedRequests { + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-40504040001"); + private static final String DOCUMENT_NUMBER = "PNOEE-40504040001-DEMO-Q"; + @Nested class Authentication { @@ -237,18 +240,18 @@ class Authentication { void initNotificationAuthentication_withSemanticIdentifier() { var request = toAuthenticationRequest(); - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, SEMANTICS_IDENTIFIER); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); } @Test void initNotificationAuthentication_withDocumentNumber() { var request = toAuthenticationRequest(); - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, "PNOEE-40504040001-DEMO-Q"); + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, DOCUMENT_NUMBER); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.sessionID())); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); } private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { @@ -277,13 +280,12 @@ class CertificateChoice { void initNotificationCertificateChoice_withSemanticIdentifier() { var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); - NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, new SemanticsIdentifier("PNOEE-40504040001")); + NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); } } - @Disabled @Nested class Signature { @@ -291,30 +293,30 @@ class Signature { void initNotificationSignature_withSemanticIdentifier() { var request = toSignatureSessionRequest(); - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); - assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); } @Test void initNotificationCertificateChoice_withDocumentNumber() { var request = toSignatureSessionRequest(); - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, "PNOEE-40504040001-MOCK-Q"); + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, DOCUMENT_NUMBER); - assertTrue(Pattern.matches(UUID_PATTERN, sessionResponse.getSessionID())); - assertTrue(Pattern.matches(VERIFICATION_CODE_PATTERN, sessionResponse.getVc().getValue())); - assertEquals("alphaNumeric4", sessionResponse.getVc().getType()); + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); } - private static SignatureSessionRequest toSignatureSessionRequest() { + private static NotificationSignatureSessionRequest toSignatureSessionRequest() { var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new SignatureSessionRequest(RELYING_PARTY_UUID, + return new NotificationSignatureSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, "QUALIFIED", SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), @@ -322,7 +324,6 @@ private static SignatureSessionRequest toSignatureSessionRequest() { null, null, InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null, null ); } diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index b4b4e792..0082e0ee 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -51,7 +51,6 @@ import org.bouncycastle.util.encoders.Base64; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -76,6 +75,7 @@ import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.Interaction; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; @@ -83,6 +83,7 @@ import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; import ee.sk.smartid.rest.dao.RequestProperties; @@ -92,7 +93,7 @@ import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SignatureSessionRequest; +import ee.sk.smartid.rest.dao.VerificationCode; import ee.sk.smartid.util.InteractionUtil; class SmartIdRestConnectorTest { @@ -1142,7 +1143,7 @@ void initCertificateChoice_onlyRequiredFields_successful() { NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); } @Test @@ -1162,7 +1163,7 @@ void initCertificateChoice_allFields_successful() { NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.getSessionID()); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); } @Test @@ -1355,7 +1356,7 @@ public void setUp() { @Test void initDeviceLinkSignature_withSemanticsIdentifier_successful() { stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); @@ -1370,7 +1371,7 @@ void initDeviceLinkSignature_withSemanticsIdentifier_successful() { void initDeviceLinkSignature_withDocumentNumber_successful() { stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); String documentNumber = "PNOEE-31111111111-MOCK-Q"; DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); @@ -1385,7 +1386,7 @@ void initDeviceLinkSignature_withDocumentNumber_successful() { @Test void initDeviceLinkSignature_userAccountNotFound() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @@ -1393,7 +1394,7 @@ void initDeviceLinkSignature_userAccountNotFound() { @Test void initDeviceLinkSignature_relyingPartyNoPermission() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @@ -1401,7 +1402,7 @@ void initDeviceLinkSignature_relyingPartyNoPermission() { @Test void initDeviceLinkSignature_invalidRequest() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @@ -1409,7 +1410,7 @@ void initDeviceLinkSignature_invalidRequest() { @Test void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); @@ -1420,7 +1421,7 @@ void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whe void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @@ -1429,7 +1430,7 @@ void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundExceptio void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } @@ -1437,7 +1438,7 @@ void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { @Test void initDeviceLinkSignature_throwsSmartIdClientException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); @@ -1447,18 +1448,18 @@ void initDeviceLinkSignature_throwsSmartIdClientException() { void initDeviceLinkSignature_throwsServerMaintenanceException() { stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); - SignatureSessionRequest request = createSignatureSessionRequest(); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18084) class SemanticsIdentifierNotificationSignature { private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); private SmartIdRestConnector connector; @@ -1467,101 +1468,98 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18084"); } - @Disabled("Request body has changed") @Test - void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); + void initNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - var semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, semanticsIdentifier); + assertSessionResponse(response); + } - assertNotNull(response); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + @Test + void initNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-all-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("DEMO", CertificateLevel.QSCD, "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", true); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertSessionResponse(response); } @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json"); + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); - }); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); } - @Disabled("Request body has changed") @Test - void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/notification-signature-session-request.json"); + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); - }); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); + connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); }); } @Test void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); - }); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); } @Test void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse( - SIGNATURE_WITH_PERSON_CODE_PATH, 480); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - - var ex = assertThrows(SmartIdClientException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); - }); + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); } @Test void initNotificationSignature_throwsServerMaintenanceException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNOEE-48010010101"); - connector.initNotificationSignature(request, semanticsIdentifier); - }); + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); } } - @Disabled("will be fixed in https://jira.sk.ee/browse/SLIB-116") @Nested @WireMockTest(httpPort = 18085) class DocumentNumberNotificationSignature { @@ -1576,45 +1574,70 @@ void setUp() { connector = new SmartIdRestConnector("http://localhost:18085"); } - @Disabled("Request body has changed") @Test - void initNotificationSignature() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json", "responses/notification-session-response.json"); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + void initNotificationSignature_onlyRequiredFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - assertNotNull(response); - assertNotNull(response.getSessionID()); - assertNotNull(response.getVc()); - assertNotNull(response.getVc().getType()); - assertNotNull(response.getVc().getValue()); + assertSessionResponse(response); } @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json"); + void initNotificationSignature_allFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + assertSessionResponse(response); } - @Disabled("Request body has changed") @Test - void initNotificationSignature_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, "requests/sign/notification/notification-signature-session-request.json"); + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("NOT DEMO", null, null, null); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + @Test void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @@ -1622,8 +1645,7 @@ void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundExcept @Test void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @@ -1631,19 +1653,16 @@ void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { @Test void initNotificationSignature_throwsSmartIdClientException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); } @Test void initNotificationSignature_throwsServerMaintenanceException() { SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); - - SignatureSessionRequest request = createNotificationSignatureSessionRequest(); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); } @@ -1695,12 +1714,12 @@ private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberR return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); } - private static SignatureSessionRequest createSignatureSessionRequest() { + private static DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", "rsassa-pss", new SignatureAlgorithmParameters("SHA3-512")); - return new SignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", + return new DeviceLinkSignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", "BANK123", null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), @@ -1712,22 +1731,27 @@ private static SignatureSessionRequest createSignatureSessionRequest() { null); } - private static SignatureSessionRequest createNotificationSignatureSessionRequest() { + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest() { + return toNotificationSignatureSessionRequest("DEMO", null, null, null); + } + + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest(String relyingPartyName, + CertificateLevel certificateLevel, + String nonce, + Boolean shareIpAddress) { var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", "rsassa-pss", new SignatureAlgorithmParameters("SHA-512")); - var interaction = new Interaction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Verify the code", null); - return new SignatureSessionRequest("00000000-0000-0000-0000-000000000000", - "DEMO", - null, + var interaction = new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Sign it!"); + return new NotificationSignatureSessionRequest("00000000-0000-4000-8000-000000000000", + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), protocolParameters, - null, + nonce, null, InteractionUtil.encodeToBase64(List.of(interaction)), - null, - null - ); + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null); } private static void assertResponseValues(DeviceLinkSessionResponse response, @@ -1743,4 +1767,13 @@ private static void assertResponseValues(DeviceLinkSessionResponse response, assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response); + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); + } } diff --git a/src/test/resources/requests/sign/notification/notification-signature-session-request.json b/src/test/resources/requests/sign/notification/notification-signature-session-request.json deleted file mode 100644 index 70732ed9..00000000 --- a/src/test/resources/requests/sign/notification/notification-signature-session-request.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "sha512WithRSAEncryption" - }, - "interactions": [ - { - "type": "verificationCodeChoice", - "displayText60": "Verify the code" - } - ] -} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json new file mode 100644 index 00000000..fea28091 --- /dev/null +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json @@ -0,0 +1,18 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QSCD", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d", + "requestProperties": { + "shareMdClientIpAddress": true + } +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json new file mode 100644 index 00000000..5a0ac32e --- /dev/null +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json @@ -0,0 +1,13 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json new file mode 100644 index 00000000..c9b762c0 --- /dev/null +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json @@ -0,0 +1,3 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +} \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json new file mode 100644 index 00000000..5d0e25f1 --- /dev/null +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json @@ -0,0 +1,13 @@ +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +} \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json new file mode 100644 index 00000000..7ba7d5d3 --- /dev/null +++ b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json @@ -0,0 +1,7 @@ +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "vc": { + "type": "numeric4", + "value": "0000" + } +} \ No newline at end of file From dae3845a4c0fe8f0be76b4ef9aa3511e4e0d7722 Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Mon, 13 Oct 2025 16:27:33 +0300 Subject: [PATCH 54/57] Improve capabilities handling for notification-based certificate choice builder (#142) * SLIB-113 - improve capabilities handling for notification certificate choice builder * SLIB-113 - improve capabilities handling for device link certificate choice builder --- ...ertificateChoiceSessionRequestBuilder.java | 3 +- ...ertificateChoiceSessionRequestBuilder.java | 3 +- .../smartid/CapabilitiesArgumentProvider.java | 4 ++- ...ficateChoiceSessionRequestBuilderTest.java | 36 ++++++------------- ...ficateChoiceSessionRequestBuilderTest.java | 12 ------- ...ionSignatureSessionRequestBuilderTest.java | 7 ++-- 6 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index f3d98dba..c5c63d46 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -36,6 +36,7 @@ import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; public class DeviceLinkCertificateChoiceSessionRequestBuilder { @@ -112,7 +113,7 @@ public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) * @return this builder */ public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index f6b6aac3..09dab6f5 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -36,6 +36,7 @@ import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; import ee.sk.smartid.rest.dao.RequestProperties; import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; /** @@ -113,7 +114,7 @@ public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce * @return this builder */ public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = Set.of(capabilities); + this.capabilities = SetUtil.toSet(capabilities); return this; } diff --git a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java index 98b362d3..dcc14ced 100644 --- a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java @@ -42,7 +42,9 @@ public Stream provideArguments(ExtensionContext context) { Arguments.of(new String[]{"capability1"}, Set.of("capability1")), Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), - Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")) + Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{" ", "capability1"}, Set.of("capability1")) ); } } diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index deba8ef4..91c63e40 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import java.net.URI; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -47,6 +48,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; @@ -114,36 +116,18 @@ void initiateCertificateChoice_missingCertificateLevel() { verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); } - @Test - void initiateCertificateChoice_withValidCapabilities() { - builderService.withCapabilities("ADVANCED", "QUALIFIED"); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullCapabilities() { - builderService.withCapabilities(); + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initiateCertificateChoice_withValidCapabilities(String[] capabilities, Set expectedCapabilities) { when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + builderService.withCapabilities(capabilities).initCertificateChoice(); - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkCertificateChoiceSessionRequest.class); + verify(connector).initDeviceLinkCertificateChoice(requestCaptor.capture()); + DeviceLinkCertificateChoiceSessionRequest request = requestCaptor.getValue(); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + assertEquals(expectedCapabilities, request.capabilities()); } @Nested diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 9fc5f516..94e9cb1c 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -35,7 +35,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Collections; import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Stream; @@ -259,17 +258,6 @@ public Stream provideArguments(ExtensionContext context) { } } - private static class CapabilitiesArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[0], Collections.emptySet()), - Arguments.of(new String[]{"capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")) - ); - } - } - private static class InvalidNonceProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index f2731d5d..55bbf052 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -249,7 +249,6 @@ void initSignatureSession_withCapabilities_ok(String[] capabilities, Set assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); } - @Nested class ErrorCases { @@ -312,7 +311,7 @@ void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() @Test void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) + () -> new NotificationSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) @@ -325,7 +324,7 @@ void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throw @Test void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) + () -> new NotificationSignatureSessionRequestBuilder(connector) .withRelyingPartyUUID(RELYING_PARTY_UUID) .withRelyingPartyName(RELYING_PARTY_NAME) .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) @@ -461,7 +460,7 @@ private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSe } private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { - var verificationCode = new VerificationCode("numeric4","4927"); + var verificationCode = new VerificationCode("numeric4", "4927"); return toNotificationSignatureSessionResponse(verificationCode); } From ab93baec22b1aecded4bd99658670044ba60bcac Mon Sep 17 00:00:00 2001 From: Urmas Muser Date: Tue, 14 Oct 2025 15:22:04 +0300 Subject: [PATCH 55/57] Update javadocs and changelong (#143) * SLIB-120 - update javadocs on exceptions * SLIB-120 - update javadocs for REST-layer classes * SLIB-120 - update javadocs for common interactions * SLIB-120 - update javadocs for utils * SLIB-120 - update javadocs; fix constants access level for CertificateParser * SLIB-120 - update changelog for v3.1 release * SLIB-120 - fix typos; improve javadoc descriptions * SLIB-120 - improve documentation about setting up SmartIDClient with SSL certificates. * SLIB-120 - add task todos --- CHANGELOG.md | 205 ++++++++---------- README.md | 28 +++ .../AuthenticationCertificateLevel.java | 12 +- .../ee/sk/smartid/AuthenticationIdentity.java | 79 ++++++- .../smartid/AuthenticationIdentityMapper.java | 14 +- .../ee/sk/smartid/AuthenticationResponse.java | 125 ++++++++++- ...ificateByDocumentNumberRequestBuilder.java | 3 + .../CertificateByDocumentNumberResult.java | 6 + .../sk/smartid/CertificateChoiceResponse.java | 56 ++++- .../java/ee/sk/smartid/CertificateLevel.java | 17 +- .../java/ee/sk/smartid/CertificateParser.java | 55 +++-- .../java/ee/sk/smartid/CertificateState.java | 11 + .../ee/sk/smartid/CertificateValidator.java | 6 + .../sk/smartid/CertificateValidatorImpl.java | 8 + ...ceLinkAuthenticationResponseValidator.java | 7 +- ...ertificateChoiceSessionRequestBuilder.java | 3 + .../java/ee/sk/smartid/DeviceLinkType.java | 24 +- src/main/java/ee/sk/smartid/DigestInput.java | 22 +- .../sk/smartid/FileTrustedCAStoreBuilder.java | 9 +- src/main/java/ee/sk/smartid/FlowType.java | 38 ++++ .../java/ee/sk/smartid/HashAlgorithm.java | 44 +++- .../java/ee/sk/smartid/MaskGenAlgorithm.java | 20 ++ ...cationAuthenticationResponseValidator.java | 7 +- .../ee/sk/smartid/RsaSsaPssParameters.java | 62 +++++- src/main/java/ee/sk/smartid/SessionType.java | 22 +- .../ee/sk/smartid/SignatureAlgorithm.java | 27 ++- ...ureCertificatePurposeValidatorFactory.java | 3 + ...ertificatePurposeValidatorFactoryImpl.java | 7 + .../java/ee/sk/smartid/SignatureProtocol.java | 17 +- .../java/ee/sk/smartid/SignatureResponse.java | 121 ++++++++++- .../smartid/SignatureResponseValidator.java | 5 +- .../sk/smartid/SignatureValueValidator.java | 7 +- .../smartid/SignatureValueValidatorImpl.java | 4 + .../java/ee/sk/smartid/SmartIdClient.java | 40 +++- src/main/java/ee/sk/smartid/TrailerField.java | 24 +- .../ee/sk/smartid/TrustedCACertStore.java | 7 +- .../ee/sk/smartid/common/InteractionType.java | 10 +- .../sk/smartid/common/InteractionsMapper.java | 1 + .../interactions/DeviceLinkInteraction.java | 15 +- .../DeviceLinkInteractionType.java | 8 +- .../interactions/NotificationInteraction.java | 16 +- .../NotificationInteractionType.java | 9 + .../exception/EnduringSmartIdException.java | 18 +- .../exception/SessionNotFoundException.java | 9 +- .../SessionSecretMismatchException.java | 9 +- .../smartid/exception/SmartIdException.java | 33 ++- ...UnprocessableSmartIdResponseException.java | 26 ++- .../exception/UserAccountException.java | 16 +- .../exception/UserActionException.java | 17 +- .../ExpectedLinkedSessionException.java | 12 +- .../permanent/ProtocolFailureException.java | 12 +- ...ingPartyAccountConfigurationException.java | 21 +- .../permanent/ServerMaintenanceException.java | 18 +- .../permanent/SmartIdClientException.java | 29 ++- .../SmartIdRequestSetupException.java | 15 +- .../permanent/SmartIdServerException.java | 10 +- .../CertificateLevelMismatchException.java | 14 +- .../DocumentUnusableException.java | 10 +- ...eAccountOfRequestedTypeFoundException.java | 15 +- ...ersonShouldViewSmartIdPortalException.java | 17 +- ...InteractionNotSupportedByAppException.java | 8 +- .../UserAccountNotFoundException.java | 13 +- .../UserAccountUnusableException.java | 7 +- .../useraction/SessionTimeoutException.java | 13 +- .../UserRefusedCertChoiceException.java | 14 +- ...erRefusedConfirmationMessageException.java | 11 +- ...essageWithVerificationChoiceException.java | 14 +- ...UserRefusedDisplayTextAndPinException.java | 14 +- .../useraction/UserRefusedException.java | 17 +- ...electedWrongVerificationCodeException.java | 14 +- .../sk/smartid/rest/SessionStatusPoller.java | 6 + .../ee/sk/smartid/rest/SmartIdConnector.java | 12 +- .../sk/smartid/rest/SmartIdRestConnector.java | 49 ++++- .../CertificateByDocumentNumberRequest.java | 7 + .../sk/smartid/rest/dao/CertificateInfo.java | 10 +- .../smartid/rest/dao/CertificateResponse.java | 6 + .../rest/dao/DeviceLinkSessionResponse.java | 16 +- .../smartid/rest/dao/SemanticsIdentifier.java | 124 ++++++++--- .../smartid/rest/dao/SessionCertificate.java | 33 ++- .../rest/dao/SessionMaskGenAlgorithm.java | 30 ++- .../SessionMaskGenAlgorithmParameters.java | 20 +- .../ee/sk/smartid/rest/dao/SessionResult.java | 40 +++- .../rest/dao/SessionResultDetails.java | 21 +- .../sk/smartid/rest/dao/SessionSignature.java | 77 ++++++- .../SessionSignatureAlgorithmParameters.java | 52 ++++- .../ee/sk/smartid/rest/dao/SessionStatus.java | 94 +++++++- .../rest/dao/SessionStatusRequest.java | 104 ++++++--- .../util/CertificateAttributeUtil.java | 3 + .../util/NationalIdentityNumberUtil.java | 43 +++- .../java/ee/sk/smartid/CertificateUtil.java | 10 +- 90 files changed, 1966 insertions(+), 421 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fbea75..31e9571b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,144 +1,129 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -Changes mentioned under 3.1.x version have not been published yet. Will be released when v3.1 is stable. - -## [3.1.17] - 2025-10-07 -- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint - -## [3.1.16] - 2025-10-04 -- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint -- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response - -## [3.1.15] - 2025-09-17 -- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest -- Updated DeviceLinkAuthenticationResponseValidator to also validate userChallenge and userChallengeVerifier same device flows. - -## [3.1.14] - 2025-09-17 -- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 -- Removed verificationCodeChoice interactions and related handling -- Removed AuthenticationHash. - -## [3.1.13] - 2025-09-08 -- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. -- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. - -## [3.1.12] - 2025-09-08 -- Removed HashType and update SignableHash and SignableData to use HashAlgorithm - -## [3.1.11] - 2025-08-25 -- Updated CertificateChoiceResponseMapper - - Renamed to CertificateChoiceResponseValidator - - Added CertificateValidator as dependency - -## [3.1.10] - 2025-08-28 -- Updated exception message of `DocumentUnusableException` - -## [3.1.9] - 2025-07-20 -- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and `SignatureResponseValidator`. - -## [3.1.8] - 2025-07-15 -- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. - -## [3.1.7] - 2025-07-10 +## [3.1-?] - TBD -- Renamed dynamic-link certificate choice to device-link certificate choice. -- Updated certificate choice endpoint to use /device-link/ paths. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. +### Structural changes -## [3.1.6] - 2025-07-08 +- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. +- Removed all Smart-ID v2 related classes, tests, and documentation. +- Updated README to reflect removal of v2-related information. -### Added -- Session-status (signature) - - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. - - Allowed `flowType`: QR · App2App · Web2App · Notification. - - Fixed `signatureAlgorithm` to `rsassa-pss`. - - `signatureAlgorithmParameters` - - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. - - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. - - `saltLength`: 32 / 48 / 64 bytes to match chosen hash. - - `trailerField`: `0xbc`. - -- Certificate - - Must be a Smart-ID *signature* certificate: - - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. - - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. - - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. - -## [3.1.5] - 2025-06-30 +### Dynamic-link auth to device-link auth changes -- Renamed dynamic-link signature to device-link signature. -- Updated signature endpoints to use /device-link/ paths. +- Renamed dynamic-link authentication to device-link authentication. +- Updated authentication endpoints to use /device-link/ paths. +- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). - Replaced signature algorithm list with fixed `rsassa-pss`. - Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. - Converted interaction list to Base64 string and ensured no duplicates. - Added `initialCallbackUrl` field with regex validation. - Added `deviceLinkBase` to session response. +- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. +- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. +- Introduced a `DeviceLinkBuilder` to generate device links. + - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. + - Ensures `elapsedSeconds` is only used for QR_CODE flows. + - Moved `deviceLinkBase` to required input (no more default). + - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. + - New payload structure includes required and optional fields as per documentation. + - `schemeName` is now configurable (default is `"smart-id"`). + - Does not store `sessionSecret`, ensures it must be passed to the build method. +- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. -## [3.1.4] - 2025-07-05 - -### Changed - Updates to session status response - - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. - - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling - - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` - - Renamed `interactionFlowUsed` to `interactionTypeUsed`. + - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. + - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling + - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` + - Renamed `interactionFlowUsed` to `interactionTypeUsed`. +- Updated exception message of `DocumentUnusableException` +- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response - Updated AuthenticationSessionRequest and related classes to records. - Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. - - Created to builder-classes for loading trusted CA certificates - - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore - - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates -- Refactored AuthenticationResponseMapper to be used as singleton instead of static class and added it as dependency for AuthenticationResponseValidator -- Update AuthenticationResponseValidator - - update signature value validation - - added additional certificate validations (validate certificate chain and certificate purpose) + - Created to builder-classes for loading trusted CA certificates + - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore + - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates +- Update AuthenticationResponseValidator to DeviceLinkAuthenticationResponseValidator + - update signature value validation + - added additional certificate validations (validate certificate chain and certificate purpose) + - added validation for userChallenge and userChallengeVerifier in case of same device flows + - added validators QualifiedAuthenticationCertificatePurposeValidator and NonQualifiedAuthenticationCertificatePurposeValidator to validate + certificate purpose based on requested certificate level. -## [3.1.3] - 2025-06-13 +- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest -### Added +### Added handling for querying certificate by document number - Added new endpoint: `POST /v3/signature/certificate/{document-number}`. - -### Removed - +- Added new builder CertificateByDocumentNumberRequestBuilder to create the request +- Add new request objects CertificateByDocumentNumberRequest and response CertificateResponse - Removed notification-based certificate choice request with document number. -## [3.1.2] - 2025-06-05 - -### Changed - -- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. -- Introduced a `DeviceLinkBuilder` to generate device-links. - - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. - - Ensures `elapsedSeconds` is only used for QR_CODE flows. - - Moved `deviceLinkBase` to required input (no more default). - - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. - - New payload structure includes required and optional fields as per documentation. - - `schemeName` is now configurable (default is `"smart-id"`). - - Does not store `sessionSecret`, ensures it must be passed to the build method. -- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. - -## [3.1.1] - 2025-06-02 +### Updated dynamic-link signature to device-link signature -### Changed - -- Renamed dynamic-link authentication to device-link authentication. -- Updated authentication endpoints to use /device-link/ paths. -- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). +- Renamed dynamic-link signature to device-link signature. +- Updated signature endpoints to use /device-link/ paths. - Replaced signature algorithm list with fixed `rsassa-pss`. - Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. - Converted interaction list to Base64 string and ensured no duplicates. - Added `initialCallbackUrl` field with regex validation. - Added `deviceLinkBase` to session response. +- Removed HashType and update SignableHash and SignableData to use HashAlgorithm +- Update signature session-status validations + - Signature + - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. + - Allowed `flowType`: QR · App2App · Web2App · Notification. + - Fixed `signatureAlgorithm` to `rsassa-pss`. + - `signatureAlgorithmParameters` + - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. + - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. + - `saltLength`: 32 / 48 / 64 bytes to match chosen hash algorithm octet length. + - `trailerField`: `0xbc`. + + - Certificate + - Must be a Smart-ID *signature* certificate: + - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or + `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. + - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. + - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. + +- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and + `SignatureResponseValidator`. + +## Update dynamic-link certificate choice to device-link certificate choice -## [3.1] - 2025-05-20 +- Renamed dynamic-link certificate choice to device-link certificate choice. +- Updated certificate choice endpoint to use /device-link/ paths. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Updated CertificateChoiceResponseMapper + - Renamed to CertificateChoiceResponseValidator + - Added CertificateValidator as dependency -### Changed -- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. -- Removed all Smart-ID v2 related classes, tests, and documentation. -- Updated README to reflect removal of v2-related information. +## Added linked signature session support + +- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. +- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. +- Added request LinkedSignatureSessionRequest and LinkedSignatureSessionResponse. + +### Updated notification-based authentication to work with Smart-ID API v3.1 + +- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 +- Removed verificationCodeChoice interactions and related handling +- Removed AuthenticationHash. +- Added NotificationAuthenticationResponseValidator + +### Updated notification-based certificate choice to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint +- Added NotificationCertificateChoiceSessionRequest + +### Updated notification-based signature to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint +- Added NotificationSignatureSessionRequest ## [3.0] - 2023-10-14 diff --git a/README.md b/README.md index 5b3108cf..80a7df8a 100644 --- a/README.md +++ b/README.md @@ -148,18 +148,46 @@ logging.level.ee.sk.smartid.rest.LoggingFilter: trace [Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO +### Setting up SSL connection to Smart-ID API + +Live SSL certificates of Smart-ID service provider (SK) can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_rp_api_smart_id_com_certificates +Demo SSL certificates can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_sid_demo_sk_ee_certificates + +Recommended way is to use truststore and provide it to the client. + ```java +// Read truststore containing Smart-ID service provider (SK) SSL certificates InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load(is, "changeit".toCharArray()); +// Initialize SmartIdClient and set connection parameters. var smartIdClient = new SmartIdClient(); +// set relying party details client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// set the trust store containing SK SSL certificates client.setTrustStore(trustStore); ``` +### Provide SSL certificates to the client + +Also it is possible to add trusted certificates one by one. + +```java +// Initialize SmartIdClient and set connection parameters. +var smartIdClient = new SmartIdClient(); +// set relying party details +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// add trusted SSL certificates +client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj..."); +``` + ## Device-link flows Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. diff --git a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java index ca008201..f5f45506 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,8 +28,18 @@ import java.util.Arrays; +/** + * Represents of authentication certificate levels. + */ public enum AuthenticationCertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow non-qualified and qualified accounts. + */ ADVANCED(1), + /** + * Smart-ID highest certificate level. Use if you want to only allow qualified accounts. + */ QUALIFIED(2); private final int level; diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index d0847509..e2559dc5 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -31,7 +31,7 @@ import java.util.Optional; /** - * Interpretation of users identity in the validate authentication certificate + * Represents users identity in the validated authentication certificate */ public class AuthenticationIdentity { @@ -42,60 +42,130 @@ public class AuthenticationIdentity { private X509Certificate authCertificate; private LocalDate dateOfBirth; + /** + * Initializes a new instance of the authentication identity. + */ public AuthenticationIdentity() { } + /** + * Initializes a new instance of authentication identity with the authentication certificate. + * + * @param authCertificate the authentication certificate where the identity information is extracted from + */ public AuthenticationIdentity(X509Certificate authCertificate) { this.authCertificate = authCertificate; } + /** + * Gets the given name of the user. + * + * @return the given name of the user + */ public String getGivenName() { return givenName; } + /** + * Sets the given name of the user. + * + * @param givenName the given name of the user + */ public void setGivenName(String givenName) { this.givenName = givenName; } + /** + * Gets the surname of the user. + * + * @return the surname of the user + */ public String getSurname() { return surname; } + /** + * Sets the surname of the user. + * + * @param surname the surname of the user + */ public void setSurname(String surname) { this.surname = surname; } + /** + * Gets the identity number of the user. + * + * @return the identity number of the user + */ public String getIdentityNumber() { return identityNumber; } + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value are only the numbers. F.e. 12345678901 + * + * @param identityNumber the identity number of the user + */ public void setIdentityNumber(String identityNumber) { this.identityNumber = identityNumber; } + /** + * Gets the identity number of the user. + * + * @return the identity code of the user + */ public String getIdentityCode() { return identityNumber; } + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value contains alphanumeric characters. F.e. EE12345678901, 1234567-8901 + * + * @param identityCode the identity code of the user + */ public void setIdentityCode(String identityCode) { this.identityNumber = identityCode; } + /** + * Gets the country code of the user. + * + * @return the country code of the user + */ public String getCountry() { return country; } + /** + * Sets the country code of the user. + * + * @param country the country code of the user + */ public void setCountry(String country) { this.country = country; } + /** + * Gets the authentication certificate of the user. + * + * @return the authentication certificate of the user + */ public X509Certificate getAuthCertificate() { return authCertificate; } /** * Person's date of birth. - * NB! This information is not available for some Latvian certificates. * * @return Date of birth if this information is available in authentication response or empty optional. */ @@ -103,6 +173,11 @@ public Optional getDateOfBirth() { return Optional.ofNullable(dateOfBirth); } + /** + * Sets person's date of birth. + * + * @param dateOfBirth Date of birth + */ public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; } diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java index 6aa0a863..bff9b6d6 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -35,7 +35,13 @@ import ee.sk.smartid.util.CertificateAttributeUtil; import ee.sk.smartid.util.NationalIdentityNumberUtil; -public class AuthenticationIdentityMapper { +/** + * Maps X509 certificate to an {@link AuthenticationIdentity} object. + */ +public final class AuthenticationIdentityMapper { + + private AuthenticationIdentityMapper() { + } /** * Maps the X509 certificate to an {@link AuthenticationIdentity} object. diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index 2e7b4183..25bdabba 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -33,16 +33,16 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; /** - * Represents the authentication response after a successful authentication sessions status response was received. + * The authentication response after a successful authentication session status response was received. *

- * Use with {@link DeviceLinkAuthenticationResponseValidator} to validate the auth certificate and signature. + * Used with {@link DeviceLinkAuthenticationResponseValidator} to validate the certificate used for authentication + * and the signature in the authentication response. */ public class AuthenticationResponse { private String endResult; private String serverRandom; private String userChallenge; - private String relyingPartyName; private String signatureValueInBase64; private X509Certificate certificate; private AuthenticationCertificateLevel certificateLevel; @@ -52,18 +52,38 @@ public class AuthenticationResponse { private String deviceIpAddress; private RsaSsaPssParameters rsaSsaPssSignatureParameters; + /** + * Gets the end result of the authentication session. + * + * @return the end result of the authentication session + */ public String getEndResult() { return endResult; } + /** + * Sets the end result of the authentication session. + * + * @param endResult the end result of the authentication session + */ public void setEndResult(String endResult) { this.endResult = endResult; } + /** + * Gets the signature value in Base64 encoding. + * + * @return signature value in Base64 encoding + */ public String getSignatureValueInBase64() { return signatureValueInBase64; } + /** + * Sets the signature value in Base64 encoding. + * + * @param signatureValueInBase64 signature value in Base64 encoding + */ public void setSignatureValueInBase64(String signatureValueInBase64) { this.signatureValueInBase64 = signatureValueInBase64; } @@ -82,82 +102,165 @@ public byte[] getSignatureValue() { } } + /** + * Get the certificate used in authentication. + * + * @return the X509Certificate used in authentication + */ public X509Certificate getCertificate() { return certificate; } + /** + * Sets the certificate used in authentication. + * + * @param certificate the X509Certificate used in authentication + */ public void setCertificate(X509Certificate certificate) { this.certificate = certificate; } + /** + * Gets the level of the authentication certificate. + * + * @return the level of the authentication certificate + */ public AuthenticationCertificateLevel getCertificateLevel() { return certificateLevel; } + /** + * Sets the level of the authentication certificate. + * + * @param certificateLevel the authentication certificate level in the session status response + */ public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; } + /** + * Gets the document number used for authentication + * + * @return the document number + */ public String getDocumentNumber() { return documentNumber; } + /** + * Sets the document number used for authentication + * + * @param documentNumber the document number from the session status response + */ public void setDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; } + /** + * Gets the interaction type used in authentication + * + * @return the interaction type used in authentication + */ public String getInteractionTypeUsed() { return interactionTypeUsed; } + /** + * Sets the interaction type used in authentication + * + * @param interactionTypeUsed the interaction type used in authentication + */ public void setInteractionTypeUsed(String interactionTypeUsed) { this.interactionTypeUsed = interactionTypeUsed; } + /** + * Gets the IP address of the device used in authentication + * + * @return the IP address of the device + */ public String getDeviceIpAddress() { return deviceIpAddress; } + /** + * Sets the IP address of the device used in authentication + * + * @param deviceIpAddress the IP address of the device + */ public void setDeviceIpAddress(String deviceIpAddress) { this.deviceIpAddress = deviceIpAddress; } + /** + * Gets the server random in Base64 encoding + * + * @return server random + */ public String getServerRandom() { return serverRandom; } + /** + * Sets the server random in Base64 encoding + * + * @param serverRandom the server random from the session status response + */ public void setServerRandom(String serverRandom) { this.serverRandom = serverRandom; } + /** + * Gets the user challenge + * + * @return user challenge + */ public String getUserChallenge() { return userChallenge; } + /** + * Sets the user challenge + * + * @param userChallenge the user challenge from the session status response + */ public void setUserChallenge(String userChallenge) { this.userChallenge = userChallenge; } - public String getRelyingPartyName() { - return relyingPartyName; - } - - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - + /** + * Gets the flow type user used to complete the authentication + *

+ * + * @return flow type + */ public FlowType getFlowType() { return flowType; } + /** + * Sets the flow type used in authentication + * + * @param flowType the flow type used in authentication + */ public void setFlowType(FlowType flowType) { this.flowType = flowType; } + /** + * Gets the RSASSA-PSS parameters + * + * @return return RSASSA-PSS parameters + */ public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { return rsaSsaPssSignatureParameters; } + /** + * Sets the RSASSA-PSS parameters + * + * @param rsaSsaPssSignatureParameters the RSASSA-PSS parameters from the session status response + */ public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; } diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java index 83580a76..ba61db1c 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -39,6 +39,9 @@ import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.util.StringUtil; +/** + * Builder for constructing request to query certificate from Smart-ID API + */ public class CertificateByDocumentNumberRequestBuilder { private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java index 7bc829d4..cafdf002 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java @@ -28,5 +28,11 @@ import java.security.cert.X509Certificate; +/** + * Result of querying certificate by document number. + * + * @param certificateLevel the level of the certificate + * @param certificate the X.509 certificate + */ public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { } diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java index aba8e37c..d9dce29a 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -37,37 +37,77 @@ public class CertificateChoiceResponse { private X509Certificate certificate; private CertificateLevel certificateLevel; private String documentNumber; - private String interactionFlowUsed; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name; Fix in SLIB-138 private String deviceIpAddress; + /** + * Gets the end result of the certificate choice session. + * + * @return the end result of the certificate choice session + */ public String getEndResult() { return endResult; } + /** + * Sets the end result of the certificate choice session. + * + * @param endResult the end result of the certificate choice session + */ public void setEndResult(String endResult) { this.endResult = endResult; } + /** + * Gets the certificate chosen by the user during the certificate choice session. + * + * @return the certificate + */ public X509Certificate getCertificate() { return certificate; } + /** + * Sets the certificate chosen by the user during the certificate choice session. + * + * @param certificate the certificate from session status response + */ public void setCertificate(X509Certificate certificate) { this.certificate = certificate; } + /** + * Gets the level of the certificate chosen by the user during the certificate choice session. + * + * @return the level of the certificate + */ public CertificateLevel getCertificateLevel() { return certificateLevel; } + /** + * Sets the level of the certificate chosen by the user during the certificate choice session. + * + * @param certificateLevel the level of the certificate from session status response + */ public void setCertificateLevel(CertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; } + /** + * Gets the document number of the user. + * + * @return the document number of the certificate + */ public String getDocumentNumber() { return documentNumber; } + /** + * Sets the document number of the certificate chosen by the user during the certificate choice session. + * + * @param documentNumber the document number of the certificate from session status response + */ public void setDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; } @@ -80,10 +120,20 @@ public void setInteractionFlowUsed(String interactionFlowUsed) { this.interactionFlowUsed = interactionFlowUsed; } + /** + * Gets the IP address of the device used in the certificate choice session. + * + * @return the IP address of the device + */ public String getDeviceIpAddress() { return deviceIpAddress; } + /** + * Sets the IP address of the device used in the certificate choice session. + * + * @param deviceIpAddress the IP address of the device from session status response + */ public void setDeviceIpAddress(String deviceIpAddress) { this.deviceIpAddress = deviceIpAddress; } diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index 8d750e79..d7373133 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -28,9 +28,24 @@ import java.util.Arrays; +/** + * Representation of different signing certificate levels. + */ public enum CertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. + */ ADVANCED(1), + + /** + * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. + */ QUALIFIED(2), + + /** + * Shortened alias for QUALIFIED level. + */ QSCD(2); private final int level; @@ -55,7 +70,7 @@ public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { * @param certificateLevel the certificate level string to check * @return true if the certificate level is supported, false otherwise */ - public static boolean isSupported(String certificateLevel){ + public static boolean isSupported(String certificateLevel) { return Arrays.stream(CertificateLevel.values()) .anyMatch(level -> level.name().equals(certificateLevel)); } diff --git a/src/main/java/ee/sk/smartid/CertificateParser.java b/src/main/java/ee/sk/smartid/CertificateParser.java index 7902337f..bbb00467 100644 --- a/src/main/java/ee/sk/smartid/CertificateParser.java +++ b/src/main/java/ee/sk/smartid/CertificateParser.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,9 +26,6 @@ * #L% */ -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; @@ -36,25 +33,41 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -public class CertificateParser { +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for parsing X509 certificates from String values. + */ +public final class CertificateParser { - public static final String END_CERT = "-----END CERTIFICATE-----"; + private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); - private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERT = "-----END CERTIFICATE-----"; - public static X509Certificate parseX509Certificate(String certificateValue) { - logger.debug("Parsing X509 certificate"); - String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( - StandardCharsets.UTF_8))); - } catch (CertificateException e) { - logger.error("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage()); - throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); + private CertificateParser() { } - } + /** + * Parses an X509 certificate from a String value. + * + * @param certificateValue the String value containing the certificate data + * @return the parsed X509Certificate + * @throws SmartIdClientException if the certificate cannot be parsed + */ + public static X509Certificate parseX509Certificate(String certificateValue) { + logger.debug("Parsing X509 certificate"); + String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( + StandardCharsets.UTF_8))); + } catch (CertificateException e) { + logger.error("Failed to parse X509 certificate from {}. Error {}", certificateString, e.getMessage()); + throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); + } + } } diff --git a/src/main/java/ee/sk/smartid/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java index bf8c3015..6ade1924 100644 --- a/src/main/java/ee/sk/smartid/CertificateState.java +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -28,8 +28,19 @@ import java.util.Arrays; +/** + * Representation state of the queried certificate from the Smart-ID API. + */ public enum CertificateState { + + /** + * Certificate is valid and can be used for signing. + */ OK, + + /** + * There is an issue with the document. + */ DOCUMENT_UNUSABLE; /** diff --git a/src/main/java/ee/sk/smartid/CertificateValidator.java b/src/main/java/ee/sk/smartid/CertificateValidator.java index 54063677..0e499c12 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidator.java +++ b/src/main/java/ee/sk/smartid/CertificateValidator.java @@ -30,6 +30,12 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +/** + * Interface for validating X509 certificates used in Smart-ID authentication and signing. + *

+ * Implementations of this interface should provide the logic to validate the certificate, + * ensuring it meets the necessary security and trust requirements. + */ public interface CertificateValidator { /** diff --git a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java index 0d21d9ae..72dec7e1 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java @@ -46,12 +46,20 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.util.CertificateAttributeUtil; +/** + * Validates the certificate's validity period and its trust chain. + */ public class CertificateValidatorImpl implements CertificateValidator { private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); private final TrustedCACertStore trustedCaCertStore; + /** + * Constructs a certificate validator with the specified trusted certificate store. + * + * @param trustedCaCertStore the store containing trusted certificates. + */ public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { this.trustedCaCertStore = trustedCaCertStore; } diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index 764f1b97..55496621 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -54,9 +54,10 @@ public class DeviceLinkAuthenticationResponseValidator { * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validator based on certificate level */ public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, AuthenticationResponseMapper authenticationResponseMapper, diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index c5c63d46..423a189e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -39,6 +39,9 @@ import ee.sk.smartid.util.SetUtil; import ee.sk.smartid.util.StringUtil; +/** + * Builder class for creating and initializing a device link-based certificate choice session. + */ public class DeviceLinkCertificateChoiceSessionRequestBuilder { private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; diff --git a/src/main/java/ee/sk/smartid/DeviceLinkType.java b/src/main/java/ee/sk/smartid/DeviceLinkType.java index 5cbf2262..0f8448aa 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkType.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkType.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,12 +27,23 @@ */ /** - * Enum for device link types + * Representation types for different device link flows. */ public enum DeviceLinkType { + /** + * QR-code (cross-device) flow. + */ QR_CODE("QR"), + + /** + * Web2App (same-device) flow. + */ WEB_2_APP("Web2App"), + + /** + * App2App (same-device) flow. + */ APP_2_APP("App2App"); private final String value; @@ -41,6 +52,11 @@ public enum DeviceLinkType { this.value = value; } + /** + * Provides the device link type as value that can be used in the Smart-ID API + * + * @return code representing the device link type + */ public String getValue() { return value; } diff --git a/src/main/java/ee/sk/smartid/DigestInput.java b/src/main/java/ee/sk/smartid/DigestInput.java index afa6d8a8..bf74c745 100644 --- a/src/main/java/ee/sk/smartid/DigestInput.java +++ b/src/main/java/ee/sk/smartid/DigestInput.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,9 +26,27 @@ * #L% */ +/** + * Represents data to be signed. + *

+ * Digest for signing can be provided either as a pre-calculated hash value {@link SignableHash} or as raw data to be hashed {@link SignableData}. + *

+ * Implementers must make sure that the getDigestInBase64() method returns the digest of the data to be in Base64-encoded format and the hashAlgorithm() + * return the correct hash algorithm used for calculating the digest or to be used for hashing the raw data. + */ public interface DigestInput { + /** + * Gets the digest in Base64-encoded string. + * + * @return the digest in base64 encoding + */ String getDigestInBase64(); + /** + * Gets the hash algorithm used for calculating the digest or to be used for hashing the raw data. + * + * @return the hash algorithm + */ HashAlgorithm hashAlgorithm(); } diff --git a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java index d9a89d35..be68443e 100644 --- a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java +++ b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -57,6 +57,11 @@ import ee.sk.smartid.util.CertificateAttributeUtil; import ee.sk.smartid.util.StringUtil; +/** + * Builder for TrustedCACertStore that loads trust anchors and intermediate CA certificates from specified keystore files. + *

+ * The builder allows configuration of trust anchor and intermediate CA keystore paths and passwords. + */ public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); diff --git a/src/main/java/ee/sk/smartid/FlowType.java b/src/main/java/ee/sk/smartid/FlowType.java index b79def14..984a5114 100644 --- a/src/main/java/ee/sk/smartid/FlowType.java +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -28,11 +28,31 @@ import java.util.Arrays; +/** + * Represents the flow types that user used to complete the authentication or signing. + */ public enum FlowType { + /** + * QR-code (cross-device) flow. User scanned a QR-code with the Smart-ID app. + */ QR("QR"), + + /** + * Web2App (same-device) flow. User clicked on a link in the browser on a mobile device + * and confirmed with the Smart-ID app. + */ WEB2APP("Web2App"), + + /** + * App2App (same-device) flow. User clicked on a link in the app on a mobile device + * and confirmed with the Smart-ID app. + */ APP2APP("App2App"), + + /** + * Notification flow. User received a push-notification and confirmed with the Smart-ID app. + */ NOTIFICATION("Notification"); private final String description; @@ -41,15 +61,33 @@ public enum FlowType { this.description = description; } + /*** + * Gets the value used in the Smart-ID API to represent the flow type. + * + * @return the flow type description + */ public String getDescription() { return description; } + /** + * Checks if the provided flow type is supported. + * + * @param flowType the flow type to check + * @return true if the flow type is supported, false otherwise + */ public static boolean isSupported(String flowType) { return Arrays.stream(FlowType.values()) .anyMatch(f -> f.getDescription().equals(flowType)); } + /** + * Converts a string representation of a flow type to the corresponding FlowType enum value. + * + * @param flowType the string representation of the flow type + * @return the corresponding FlowType enum value + * @throws IllegalArgumentException if the provided flow type is not valid + */ public static FlowType fromString(String flowType) { return Arrays.stream(FlowType.values()) .filter(f -> f.getDescription().equals(flowType)) diff --git a/src/main/java/ee/sk/smartid/HashAlgorithm.java b/src/main/java/ee/sk/smartid/HashAlgorithm.java index 173a9ffb..f7e74a9d 100644 --- a/src/main/java/ee/sk/smartid/HashAlgorithm.java +++ b/src/main/java/ee/sk/smartid/HashAlgorithm.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,13 +29,37 @@ import java.util.Arrays; import java.util.Optional; +/** + * Represents hash algorithms used for hashing the data to be signed. + *

+ * * The algorithm name is the name used in the Smart-ID API. + * * The octet length represents salt length in bytes (octets) produced by the hash algorithm. + */ public enum HashAlgorithm { + /** + * SHA-256 - produces 32 bytes (256 bit) hash + */ SHA_256("SHA-256", 32), + /** + * SHA-384 - produces 48 bytes (384 bit) hash + */ SHA_384("SHA-384", 48), + /** + * SHA-384 - produces 64 bytes (512 bit) hash + */ SHA_512("SHA-512", 64), + /** + * SHA3-256 - produces 32 bytes (256 bit) hash + */ SHA3_256("SHA3-256", 32), + /** + * SHA3-384 - produces 48 bytes (384 bit) hash + */ SHA3_384("SHA3-384", 48), + /** + * SHA3-384 - produces 64 bytes (512 bit) hash + */ SHA3_512("SHA3-512", 64); private final String algorithmName; @@ -46,14 +70,30 @@ public enum HashAlgorithm { this.octetLength = octetLength; } + /** + * Gets the name of the algorithm as used in the Smart-ID API. + * + * @return the algorithm name + */ public String getAlgorithmName() { return algorithmName; } + /** + * Gets the length of the hash in bytes (octets). + * + * @return the octet length + */ public int getOctetLength() { return octetLength; } + /** + * Find HashAlgorithm by its name. + * + * @param input the name of the algorithm + * @return an Optional containing the HashAlgorithm if found, or an empty Optional if not found + */ public static Optional fromString(String input) { return Arrays.stream(HashAlgorithm.values()) .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) diff --git a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java index 63b8fc93..dad59496 100644 --- a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -33,6 +33,9 @@ */ public enum MaskGenAlgorithm { + /** + * id-mgf1 is used in the Smart-ID API and MGF1 is the name used in the Java Cryptography API. + */ ID_MGF1("id-mgf1", "MGF1"); private final String algorithmName; @@ -43,14 +46,31 @@ public enum MaskGenAlgorithm { this.mgfName = mgfName; } + /** + * Gets the algorithm name used by the Smart-ID API. + * + * @return the algorithm name + */ public String getAlgorithmName() { return algorithmName; } + /** + * Gets the MGF name used in the Java Cryptography API. + * + * @return the MGF name + */ public String getMgfName() { return mgfName; } + /** + * Converts a string to the corresponding MaskGenAlgorithm enum value. + * + * @param maskGenAlgorithm the string representation of the mask generation algorithm + * @return the corresponding MaskGenAlgorithm enum value + * @throws IllegalArgumentException if the provided string does not match any enum value + */ public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { return Arrays.stream(MaskGenAlgorithm.values()) .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java index b0df9e7e..0efb494e 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -53,9 +53,10 @@ public class NotificationAuthenticationResponseValidator { * Creates an instance of {@link NotificationAuthenticationResponseValidator} * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validators based on certificate level */ public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, AuthenticationResponseMapper authenticationResponseMapper, diff --git a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java index 68780813..81bc797f 100644 --- a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java +++ b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,6 +26,9 @@ * #L% */ +/** + * Encapsulates multiple parameters of RSASSA-PSS + */ public class RsaSsaPssParameters { private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; @@ -36,46 +39,101 @@ public class RsaSsaPssParameters { private int saltLength; private TrailerField trailerField; + /** + * Sets the hash algorithm + * + * @param digestHashAlgorithm the hash algorithm; see {@link HashAlgorithm} + */ public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { this.digestHashAlgorithm = digestHashAlgorithm; } + /** + * Sets the mask generation algorithm + * + * @param maskGenAlgorithm the mask generation algorithm; see {@link MaskGenAlgorithm} + */ public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { this.maskGenAlgorithm = maskGenAlgorithm; } + /** + * Sets the mask hash algorithm + * + * @param maskHashAlgorithm the mask hash algorithm; see {@link HashAlgorithm} + */ public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { this.maskHashAlgorithm = maskHashAlgorithm; } + /** + * Sets the salt length + * + * @param saltLength the salt length in bytes + */ public void setSaltLength(int saltLength) { this.saltLength = saltLength; } + /** + * Sets the trailer field + * + * @param trailerField the trailer field; see {@link TrailerField} + */ public void setTrailerField(TrailerField trailerField) { this.trailerField = trailerField; } + /** + * Gets the signature algorithm + * + * @return the signature algorithm; see {@link SignatureAlgorithm} + */ public SignatureAlgorithm getSignatureAlgorithm() { return signatureAlgorithm; } + /** + * Gets the hash algorithm + * + * @return the hash algorithm; see {@link HashAlgorithm} + */ public HashAlgorithm getDigestHashAlgorithm() { return digestHashAlgorithm; } + /** + * Gets the mask generation algorithm + * + * @return the mask generation algorithm + */ public MaskGenAlgorithm getMaskGenAlgorithm() { return maskGenAlgorithm; } + /** + * Gets the mask hash algorithm + * + * @return the mask hash algorithm + */ public HashAlgorithm getMaskHashAlgorithm() { return maskHashAlgorithm; } + /** + * Gets the salt length + * + * @return the salt length in bytes + */ public int getSaltLength() { return saltLength; } + /** + * Gets the trailer field + * + * @return the trailer field + */ public TrailerField getTrailerField() { return trailerField; } diff --git a/src/main/java/ee/sk/smartid/SessionType.java b/src/main/java/ee/sk/smartid/SessionType.java index 0acf1137..be0b8b0f 100644 --- a/src/main/java/ee/sk/smartid/SessionType.java +++ b/src/main/java/ee/sk/smartid/SessionType.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,12 +27,21 @@ */ /** - * Enum for session types + * Represents session types used to construct different device links for Smart-ID sessions. */ public enum SessionType { + /** + * Authentication session type + */ AUTHENTICATION("auth"), + /** + * Signature session type + */ SIGNATURE("sign"), + /** + * Certificate choice session type + */ CERTIFICATE_CHOICE("cert"); private final String value; @@ -41,6 +50,11 @@ public enum SessionType { this.value = value; } + /** + * Returns the value used in the device link for the session type. + * + * @return the string value of the session type. + */ public String getValue() { return value; } diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index 6d4cc9e5..f6a33872 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,8 +28,15 @@ import java.util.Arrays; +/** + * Signature algorithms supported by Smart-ID API. + */ public enum SignatureAlgorithm { + /** + * RSASSA-PSS (RSA Probabilistic Signature Scheme) as defined in PKCS #1 v2.1. + * This algorithm provides probabilistic signature generation for enhanced security. + */ RSASSA_PSS("rsassa-pss"); private final String algorithmName; @@ -38,15 +45,33 @@ public enum SignatureAlgorithm { this.algorithmName = algorithmName; } + /** + * Provides the signature algorithm name as used in the Smart-ID API. + * + * @return the signature algorithm name + */ public String getAlgorithmName() { return algorithmName; } + /** + * Checks if the provided signature algorithm is supported. + * + * @param signatureAlgorithm the signature algorithm name to check + * @return true if the signature algorithm is supported, false otherwise + */ public static boolean isSupported(String signatureAlgorithm) { return Arrays.stream(SignatureAlgorithm.values()) .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); } + /** + * Converts a string representation of a signature algorithm to its corresponding enum value. + * + * @param signatureAlgorithm the signature algorithm name + * @return the corresponding SignatureAlgorithm enum value + * @throws IllegalArgumentException if the provided signature algorithm is not supported + */ public static SignatureAlgorithm fromString(String signatureAlgorithm) { return Arrays .stream(SignatureAlgorithm.values()) diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java index 41b039d1..972c4704 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java @@ -26,6 +26,9 @@ * #L% */ +/** + * Factory interface to create instances of SignatureCertificatePurposeValidator based on the certificate level. + */ public interface SignatureCertificatePurposeValidatorFactory { /** diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java index d89f9b80..1f07b2c3 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java @@ -28,6 +28,13 @@ import ee.sk.smartid.exception.permanent.SmartIdClientException; +/** + * Factory to create Qualified or Non-Qualified SignatureCertificatePurposeValidator based on the certificate level. + * Will be used to validate the certificate purpose of the signature certificate. + *

+ * Only QUALIFIED and ADVANCED certificate levels are supported, + * because QUALIFIED level certificate will also be returned for QSCD. + */ public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { @Override diff --git a/src/main/java/ee/sk/smartid/SignatureProtocol.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java index 111dcdae..4ee32e67 100644 --- a/src/main/java/ee/sk/smartid/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,18 @@ * #L% */ +/** + * Signature protocols supported by Smart-ID API. + */ public enum SignatureProtocol { + + /** + * Signature protocol used for authentication. + */ ACSP_V2, + + /** + * Signature protocol used for signature. + */ RAW_DIGEST_SIGNATURE } diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index 9e7c00cb..31e933d5 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -32,6 +32,9 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +/** + * Response of a completed and validated signature session. + */ public class SignatureResponse implements Serializable { private String endResult; @@ -43,10 +46,16 @@ public class SignatureResponse implements Serializable { private CertificateLevel requestedCertificateLevel; private CertificateLevel certificateLevel; private String documentNumber; - private String interactionFlowUsed; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name 'interactionTypeUsed'; Fix in SLIB-138 private String deviceIpAddress; private RsaSsaPssParameters rsaSsaPssParameters; + /** + * Gets the signature value as a byte array by decoding the base64-encoded string. + * + * @return the signature value as a byte array + * @throws UnprocessableSmartIdResponseException if the base64 string is incorrectly encoded + */ public byte[] getSignatureValue() { try { return Base64.getDecoder().decode(signatureValueInBase64); @@ -56,74 +65,164 @@ public byte[] getSignatureValue() { } } + /** + * Gets the end result of the signing operation. + *

+ * returns the end result of the signing operation + */ public String getEndResult() { return endResult; } + /** + * Sets the end result of the signing operation. + * + * @param endResult the end result of the signing operation + */ public void setEndResult(String endResult) { this.endResult = endResult; } + /** + * Gets the signature value as a base64-encoded string. + * + * @return the signature value in base64 + */ public String getSignatureValueInBase64() { return signatureValueInBase64; } + /** + * Sets the signature value as a base64-encoded string. + * + * @param signatureValueInBase64 the signature value in base64 + */ public void setSignatureValueInBase64(String signatureValueInBase64) { this.signatureValueInBase64 = signatureValueInBase64; } + /** + * Gets the name of the algorithm used for signing. + * + * @return the name of the algorithm + */ public String getAlgorithmName() { return algorithmName; } + /** + * Sets the name of the algorithm used for signing. + * + * @param algorithmName the name of the algorithm + */ public void setAlgorithmName(String algorithmName) { this.algorithmName = algorithmName; } + /** + * Gets the signature algorithm used for signing. + * + * @return the signature algorithm + */ public SignatureAlgorithm getSignatureAlgorithm() { return signatureAlgorithm; } + /** + * Sets the signature algorithm used for signing. + * + * @param signatureAlgorithm the signature algorithm + */ public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } + /** + * Gets the flow type user used to complete the signing. + * + * @return the flow type + */ public FlowType getFlowType() { return flowType; } + /** + * Sets the flow type. + * + * @param flowType the flow type + */ public void setFlowType(FlowType flowType) { this.flowType = flowType; } + /** + * Gets the certificate used for signing. + * + * @return the X.509 certificate + */ public X509Certificate getCertificate() { return certificate; } + /** + * Sets the certificate used for signing. + * + * @param certificate the X.509 certificate + */ public void setCertificate(X509Certificate certificate) { this.certificate = certificate; } + /** + * Gets the certificate level of the certificate used for signing. + * + * @return the certificate level + */ public CertificateLevel getCertificateLevel() { return certificateLevel; } + /** + * Sets the certificate level of the certificate used for signing. + * + * @param certificateLevel the certificate level + */ public void setCertificateLevel(CertificateLevel certificateLevel) { this.certificateLevel = certificateLevel; } + /** + * Gets the requested certificate level for the signing operation. + * + * @return the requested certificate level + */ public CertificateLevel getRequestedCertificateLevel() { return requestedCertificateLevel; } + /** + * Sets the requested certificate level for the signing operation. + * + * @param requestedCertificateLevel the requested certificate level + */ public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { this.requestedCertificateLevel = requestedCertificateLevel; } + /** + * Gets the document number of the user who performed the signing. + * + * @return the document number + */ public String getDocumentNumber() { return documentNumber; } + /** + * Sets the document number of the user who performed the signing. + * + * @param documentNumber the document number + */ public void setDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; } @@ -136,18 +235,38 @@ public void setInteractionFlowUsed(String interactionFlowUsed) { this.interactionFlowUsed = interactionFlowUsed; } + /** + * Gets the IP address of the device used by the user to complete the signing. + * + * @return the device IP address + */ public String getDeviceIpAddress() { return deviceIpAddress; } + /** + * Sets the IP address of the device. + * + * @param deviceIpAddress the device IP address + */ public void setDeviceIpAddress(String deviceIpAddress) { this.deviceIpAddress = deviceIpAddress; } + /** + * Gets the RSASSA-PSS parameters used in the signing operation. + * + * @return the RSASSA-PSS parameters. + */ public RsaSsaPssParameters getRsaSsaPssParameters() { return rsaSsaPssParameters; } + /** + * Sets the RSASSA-PSS parameters used in the signing operation. + * + * @param rsaSsaPssParameters the RSASSA-PSS parameters. + */ public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { this.rsaSsaPssParameters = rsaSsaPssParameters; } diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index 44ee19e1..63b85de4 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -51,6 +51,9 @@ import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.util.StringUtil; +/** + * Validator for signature session status. + */ public class SignatureResponseValidator { private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); @@ -82,7 +85,7 @@ public SignatureResponseValidator(CertificateValidator certificateValidator) { } /** - * Create {@link SignatureResponse} from {@link SessionStatus} + * Validates {@link SessionStatus} and produces {@link SignatureResponse}. * * @param sessionStatus session status response * @param requestedCertificateLevel certificate level used to start the signature session diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidator.java b/src/main/java/ee/sk/smartid/SignatureValueValidator.java index 628518cc..5df475af 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidator.java @@ -28,6 +28,11 @@ import java.security.cert.X509Certificate; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for signature value validator. + */ public interface SignatureValueValidator { /** @@ -37,7 +42,7 @@ public interface SignatureValueValidator { * @param payload the original data that was signed * @param certificate X509 certificate used for signature validation * @param rsaSsaPssParameters signature parameters used for creating signature value - * @throws UnsupportedOperationException when there are any issue with validating the signature value + * @throws UnprocessableSmartIdResponseException when there are any issue with validating the signature value */ void validate(byte[] signatureValue, byte[] payload, diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java index ee73edf1..678f5f9d 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -40,6 +40,10 @@ import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; import ee.sk.smartid.exception.permanent.SmartIdClientException; +/** + * Implementation of {@link SignatureValueValidator} that uses RSASSA-PSS signature algorithm + * to validate the signature value in the authentication and signature session status response. + */ public final class SignatureValueValidatorImpl implements SignatureValueValidator { private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 490f9bd0..a3ba1def 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -51,6 +51,9 @@ import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.Configuration; +/** + * Main entry point for using Smart-ID services. + */ public class SmartIdClient { private String relyingPartyUUID; @@ -246,6 +249,11 @@ public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { this.networkConnectionConfig = networkConnectionConfig; } + /** + * Set the configured client. + * + * @param configuredClient jakarta.ws.rs.client.Client implementations + */ public void setConfiguredClient(Client configuredClient) { this.configuredClient = configuredClient; } @@ -322,10 +330,7 @@ public void setTrustSslContext(SSLContext trustSslContext) { } /** - * Sets the trust store for the client - *

- * Useful for configuring custom trust store - * for the client. + * Set trust store containing SSL certificates * * @param trustStore trust store for the client */ @@ -341,6 +346,11 @@ public void setTrustStore(KeyStore trustStore) { } } + /** + * Can be used instead of {@link #setTrustStore} to add SSL certificates. + * + * @param sslCertificates certificates in PEM format + */ public void setTrustedCertificates(String... sslCertificates) { try { this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); @@ -349,6 +359,14 @@ public void setTrustedCertificates(String... sslCertificates) { } } + /** + * Sets the smart-id connector + *

+ * Useful for providing custom implementation + * of the connector. + * + * @param smartIdConnector smart-id connector + */ public void setSmartIdConnector(SmartIdConnector smartIdConnector) { this.connector = smartIdConnector; } @@ -369,11 +387,11 @@ private Client createClient() { * * @param sslCertificates list of certificates in PEM format * @return SSL context - * @throws NoSuchAlgorithmException - * @throws KeyStoreException - * @throws IOException - * @throws CertificateException - * @throws KeyManagementException + * @throws NoSuchAlgorithmException if SSL context with provided protocol is not found + * @throws KeyStoreException if key store cannot be created for a type + * @throws IOException if loading key store fails + * @throws CertificateException if certificate for the given data cannot be generated + * @throws KeyManagementException if SSL context cannot be initialized */ public static SSLContext createSslContext(List sslCertificates) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index 91ce118f..b8482bb8 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -29,11 +29,14 @@ import java.util.Arrays; /** - * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response. - * The pssSpecValue necessary for generating the signature. + * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response + * and related value for PSSParameterSpec. */ public enum TrailerField { + /** + * Trailer field hexadecimal value "0xbc" with PSSParameterSpec value 1. + */ BC("0xbc", 1); private final String value; @@ -44,14 +47,31 @@ public enum TrailerField { this.pssSpecValue = pssSpecValue; } + /** + * Gets the hexadecimal value of the trailer field. + * + * @return the hexadecimal value of the trailer field. + */ public String getValue() { return value; } + /** + * Gets the PSSParameterSpec value associated with the trailer field. + * + * @return the PSS specification value. + */ public int getPssSpecValue() { return pssSpecValue; } + /** + * Converts a string representation of a trailer field to its corresponding TrailerField enum value. + * + * @param trailerField the string representation of the trailer field. + * @return the corresponding TrailerField enum value. + * @throws IllegalArgumentException if the provided string does not match any TrailerField value. + */ public static TrailerField fromString(String trailerField) { return Arrays.stream(TrailerField.values()) .filter(field -> field.getValue().equals(trailerField)) diff --git a/src/main/java/ee/sk/smartid/TrustedCACertStore.java b/src/main/java/ee/sk/smartid/TrustedCACertStore.java index 84d10de1..c2ebc210 100644 --- a/src/main/java/ee/sk/smartid/TrustedCACertStore.java +++ b/src/main/java/ee/sk/smartid/TrustedCACertStore.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,6 +31,9 @@ import java.util.List; import java.util.Set; +/** + * Interface for a store of trusted CA certificates and trust anchors. + */ public interface TrustedCACertStore { /** diff --git a/src/main/java/ee/sk/smartid/common/InteractionType.java b/src/main/java/ee/sk/smartid/common/InteractionType.java index 722e0ff2..4063127d 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionType.java +++ b/src/main/java/ee/sk/smartid/common/InteractionType.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,19 +27,19 @@ */ /** - * Representations of interaction types that can be used in authentication and signing requests + * Interface for interaction types that can be used in authentication and signing requests. */ public interface InteractionType { /** - * Provides the interaction type as value that can be used in the Smart ID API + * Get the interaction type as value that can be used in the Smart ID API. * * @return code representing the interaction type */ String getCode(); /** - * Provides the maximum length of the display text for this interaction type + * Get the maximum length of the display text for this interaction type. * * @return maximum length of the display text */ diff --git a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java index acc37671..c56e52ae 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -42,6 +42,7 @@ private InteractionsMapper() { /** * Converts from any SmartIdInteraction to Interaction * + * @param type of SmartIdInteraction * @param interaction the interaction to be converted * @return interaction to be used in REST request */ diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java index fd3b201d..29803eee 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,7 +31,7 @@ import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; /** - * DeviceLink interaction to be used in device-link based authentication and signing requests + * Interaction to be used in device-link based authentication and signing requests * * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). @@ -41,6 +41,15 @@ public record DeviceLinkInteraction(DeviceLinkInteractionType type, String displayText60, String displayText200) implements SmartIdInteraction { + /** + * Creates a new instance of {@link DeviceLinkInteraction}. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type interaction type (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ public DeviceLinkInteraction { if (type == null) { throw new SmartIdRequestSetupException("Value for 'type' must be set"); diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java index 17214154..2c1c1bda 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java @@ -29,11 +29,17 @@ import ee.sk.smartid.common.InteractionType; /** - * Device link interaction types that can be used in device link based authentication and signing requests + * Interaction types that can be used in device link-based authentication and signing requests */ public enum DeviceLinkInteractionType implements InteractionType { + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ CONFIRMATION_MESSAGE("confirmationMessage", 200); private final String code; diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java index 6aac6a73..7981f9fb 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,7 +33,7 @@ import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; /** - * Notification interaction to be used in notification based authentication and signing requests + * Interaction to be used in notification-based authentication and signing requests * * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). @@ -43,6 +43,16 @@ public record NotificationInteraction(NotificationInteractionType type, String displayText60, String displayText200) implements Serializable, SmartIdInteraction { + /** + * Constructs a new NotificationInteraction instance. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @throws SmartIdRequestSetupException if display text fields have incorrect value based on the type + */ public NotificationInteraction { if (type == null) { throw new SmartIdRequestSetupException("Value for 'type' must be set"); diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java index 9e8e3c3f..80f88363 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java @@ -33,8 +33,17 @@ */ public enum NotificationInteractionType implements InteractionType { + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ CONFIRMATION_MESSAGE("confirmationMessage", 200), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog and verification code choice before entering the PIN. + */ CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); private final String code; diff --git a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java index 8d8e31ba..fb625ec6 100644 --- a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,10 +33,22 @@ * With these types of errors there is not recommended to ask the user for immediate retry. */ public abstract class EnduringSmartIdException extends SmartIdException { + + /** + * Constructs the exception with the specified message. + * + * @param message the message to describe the reason for the exception + */ public EnduringSmartIdException(String message) { super(message); } + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message to describe the reason for the exception + * @param cause the underlying cause of the exception + */ public EnduringSmartIdException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java index b73720ae..e126e25e 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,5 +26,8 @@ * #L% */ +/** + * Thrown when session with the given session ID could not be found. + */ public class SessionNotFoundException extends SmartIdException { } diff --git a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java index f73b00aa..5736ac83 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,6 +31,11 @@ */ public class SessionSecretMismatchException extends SmartIdException { + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ public SessionSecretMismatchException(String message) { super(message); } diff --git a/src/main/java/ee/sk/smartid/exception/SmartIdException.java b/src/main/java/ee/sk/smartid/exception/SmartIdException.java index 39afca25..b6fd4b2f 100644 --- a/src/main/java/ee/sk/smartid/exception/SmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/SmartIdException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,14 +31,25 @@ */ public abstract class SmartIdException extends RuntimeException { - public SmartIdException() { - } + public SmartIdException() { + } - public SmartIdException(String message) { - super(message); - } + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdException(String message) { + super(message); + } - public SmartIdException(String message, Throwable cause) { - super(message, cause); - } + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the underlying cause of this exception. + */ + public SmartIdException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java index 7ca82da7..57c4193a 100644 --- a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java +++ b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,16 +26,30 @@ * #L% */ - import ee.sk.smartid.exception.permanent.SmartIdClientException; +/** + * Thrown when validation of any Smart-ID API responses fail. + * This includes responses for session initialization requests and session status responses. + */ public class UnprocessableSmartIdResponseException extends SmartIdClientException { + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ public UnprocessableSmartIdResponseException(String message) { super(message); } - public UnprocessableSmartIdResponseException(String s, Exception e) { - super(s, e); + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param exception the exception that caused this exception to be thrown. + */ + public UnprocessableSmartIdResponseException(String message, Exception exception) { + super(message, exception); } } diff --git a/src/main/java/ee/sk/smartid/exception/UserAccountException.java b/src/main/java/ee/sk/smartid/exception/UserAccountException.java index a9b7f5c0..d964f758 100644 --- a/src/main/java/ee/sk/smartid/exception/UserAccountException.java +++ b/src/main/java/ee/sk/smartid/exception/UserAccountException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,8 +31,14 @@ * General practise is to display a notification and ask user to log in to Smart-ID self-service portal. */ public abstract class UserAccountException extends SmartIdException { - public UserAccountException(String s) { - super(s); + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserAccountException(String message) { + super(message); } } diff --git a/src/main/java/ee/sk/smartid/exception/UserActionException.java b/src/main/java/ee/sk/smartid/exception/UserActionException.java index 8793b1cb..56cf6cd0 100644 --- a/src/main/java/ee/sk/smartid/exception/UserActionException.java +++ b/src/main/java/ee/sk/smartid/exception/UserActionException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,8 +31,13 @@ * General practise is to ask the user to try again. */ public abstract class UserActionException extends SmartIdException { - public UserActionException(String s) { - super(s); - } + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserActionException(String message) { + super(message); + } } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java index f5320d26..00372c2f 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,8 +28,16 @@ import ee.sk.smartid.exception.EnduringSmartIdException; +/** + * Linked signature flow consists of two sessions - device link-based certificate choice session followed by the linked signature session. + * Exception will be thrown when linked signature session is not received after the device link-based certificate choice session, + * but some other session with the same document number is received instead. + */ public class ExpectedLinkedSessionException extends EnduringSmartIdException { + /** + * Constructs the exception with default message. + */ public ExpectedLinkedSessionException() { super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java index f7a117ee..bb262cd7 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,8 +28,16 @@ import ee.sk.smartid.exception.EnduringSmartIdException; +/** + * Exception thrown when the session status end result is PROTOCOL_FAILURE, indicating logical error in the signing protocol. + *

+ * F.e. Constructed device link that user can interact with contains invalid schema. + */ public class ProtocolFailureException extends EnduringSmartIdException { + /** + * Constructs the exception with default message. + */ public ProtocolFailureException() { super("A logical error occurred in the signing protocol."); } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java index 6f191137..d618d1f9 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,11 +27,20 @@ */ /** - * Problems with RelyingParty account and access configuration + * Exception will be thrown when there are problems with relying party account and access configuration + * or when relying party does not have access to the requested service. + *

+ * F.e. Request is made with relying party UUID and incorrect relying party name. */ public class RelyingPartyAccountConfigurationException extends SmartIdClientException { - public RelyingPartyAccountConfigurationException(String s, Exception e) { - super(s, e); + /** + * Constructs the exception with message and cause. + * + * @param message the exception message + * @param exception underlying cause for this exception + */ + public RelyingPartyAccountConfigurationException(String message, Exception exception) { + super(message, exception); } } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java index ddd7bb07..fc46fbd6 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,9 +28,15 @@ import ee.sk.smartid.exception.EnduringSmartIdException; +/** + * Thrown when request cannot be process because the Smart-ID API server is under maintenance. + */ public class ServerMaintenanceException extends EnduringSmartIdException { - public ServerMaintenanceException() { - super("Server is under maintenance, retry later."); - } + /** + * Constructs the exception with default message. + */ + public ServerMaintenanceException() { + super("Server is under maintenance, retry later."); + } } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java index 4c710ee5..e40589e8 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,11 +33,22 @@ */ public class SmartIdClientException extends EnduringSmartIdException { - public SmartIdClientException(String message) { - super(message); - } + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdClientException(String message) { + super(message); + } - public SmartIdClientException(String message, Throwable cause) { - super(message, cause); - } + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ + public SmartIdClientException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java index 4088fa86..3cf49946 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -33,10 +33,21 @@ */ public class SmartIdRequestSetupException extends SmartIdClientException { + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ public SmartIdRequestSetupException(String message) { super(message); } + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ public SmartIdRequestSetupException(String message, Exception cause) { super(message, cause); } diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java index 9e3c7a71..b0d84fe5 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,8 +28,14 @@ import ee.sk.smartid.exception.EnduringSmartIdException; +/** + * Thrown when session status end result is SERVER_ERROR, indicating a server-side technical error. + */ public class SmartIdServerException extends EnduringSmartIdException { + /** + * Constructs the exception with default message. + */ public SmartIdServerException() { super("Process was terminated due to server-side technical error"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java index 2a1b73b5..4db92fb2 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,15 +26,25 @@ * #L% */ - import ee.sk.smartid.exception.UserAccountException; +/** + * Thrown when returned certificate level is lower than the requested certificate level. + */ public class CertificateLevelMismatchException extends UserAccountException { + /** + * Constructs the exception with the default message. + */ public CertificateLevelMismatchException() { super("Signer's certificate is below requested certificate level"); } + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message + */ public CertificateLevelMismatchException(String message) { super(message); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java index 572e7fb9..2df63e82 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,8 +26,14 @@ * #L% */ +/** + * Thrown when session status end result is DOCUMENT_UNUSABLE. + */ public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { + /** + * Constructs the exception with default message. + */ public DocumentUnusableException() { super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java index 3de1b943..86a1d0e6 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,8 +28,17 @@ import ee.sk.smartid.exception.UserAccountException; +/** + * Thrown when user does not have a suitable account for the requested operation. + *

+ * F.e. user has non-qualified account with ADVANCED certificate level, + * but QUALIFIED certificate level is required for the operation. + */ public class NoSuitableAccountOfRequestedTypeFoundException extends UserAccountException { + /** + * Constructs the exception with default message. + */ public NoSuitableAccountOfRequestedTypeFoundException() { super("No suitable account of requested type found, but user has some other accounts."); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java index 3e6dce4b..1a3b0b87 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2022 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,12 +28,23 @@ import ee.sk.smartid.exception.UserAccountException; +/** + * Thrown when Smart-ID API indicates that there is an issue with user document and user should check its state. + */ public class PersonShouldViewSmartIdPortalException extends UserAccountException { + /** + * Constructs the exception with default message. + */ public PersonShouldViewSmartIdPortalException() { super("Person should view Smart-ID app or Smart-ID self-service portal now."); } + /** + * Constructs the exception with the specified exception message. + * + * @param message exception message + */ public PersonShouldViewSmartIdPortalException(String message) { super(message); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java index 436ca2ed..c2b9a14c 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,8 +28,14 @@ import ee.sk.smartid.exception.UserAccountException; +/** + * Thrown when the user's app version does not support any of the provided interactions. + */ public class RequiredInteractionNotSupportedByAppException extends UserAccountException { + /** + * Constructs the exception with the default message. + */ public RequiredInteractionNotSupportedByAppException() { super("User app version does not support any of the provided interactions."); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java index 85774c57..725af6ec 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,7 +28,14 @@ import ee.sk.smartid.exception.UserAccountException; +/** + * Thrown when user account does not exist with the given identifier or document number. + */ public class UserAccountNotFoundException extends UserAccountException { + + /** + * Constructs the exception with message. + */ public UserAccountNotFoundException() { super("User account not found"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java index cc17173c..62e99b77 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java @@ -28,9 +28,14 @@ import ee.sk.smartid.exception.UserAccountException; - +/** + * Thrown when session status end result is ACCOUNT_UNUSABLE. + */ public class UserAccountUnusableException extends UserAccountException { + /** + * Constructs the exception with the default exception message. + */ public UserAccountUnusableException() { super("The account is currently unusable"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java index 2726e7ea..ce86589b 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,7 +28,14 @@ import ee.sk.smartid.exception.UserActionException; +/** + * Thrown when session status end result is TIMEOUT. + */ public class SessionTimeoutException extends UserActionException { + + /** + * Constructs the exception with default message. + */ public SessionTimeoutException() { super("Session timed out without getting any response from user"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java index 1b403f8e..edea95a6 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,15 @@ * #L% */ +/** + * Thrown when session status end result is USER_REFUSED_CERT_CHOICE. + * This happens when user has multiple accounts and presses Cancel on device choice screen on any device. + */ public class UserRefusedCertChoiceException extends UserRefusedException { + + /** + * Constructs a new UserRefusedDisplayTextAndPinException with the default exception message. + */ public UserRefusedCertChoiceException() { super("User has multiple accounts and pressed Cancel on device choice screen on any device."); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java index 47826583..8444d5c3 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,8 +26,15 @@ * #L% */ +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation message screen. + */ public class UserRefusedConfirmationMessageException extends UserRefusedException { + /** + * Constructs the exception with the default exception message. + */ public UserRefusedConfirmationMessageException() { super("User cancelled on confirmationMessage screen"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java index 66091a14..06a3c5a4 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,15 @@ * #L% */ +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation and verification code choice screen. + */ public class UserRefusedConfirmationMessageWithVerificationChoiceException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ public UserRefusedConfirmationMessageWithVerificationChoiceException() { super("User cancelled on confirmationMessageAndVerificationCodeChoice screen"); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java index 89d3e4fe..723d0a18 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,15 @@ * #L% */ +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on display text and PIN screen. + */ public class UserRefusedDisplayTextAndPinException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ public UserRefusedDisplayTextAndPinException() { super("User pressed Cancel on PIN screen."); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java index 1b71cb30..d729292d 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,12 +28,23 @@ import ee.sk.smartid.exception.UserActionException; +/** + * Thrown when session status end result is USER_REFUSED. + */ public class UserRefusedException extends UserActionException { + /** + * Constructs the exception with the default exception message. + */ public UserRefusedException() { super("User pressed cancel in app"); } + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ public UserRefusedException(String message) { super(message); } diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java index 196dfbd9..24b4256b 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -28,7 +28,15 @@ import ee.sk.smartid.exception.UserActionException; +/** + * Thrown when session status result is WRONG_VC. + * This happens when user selects wrong verification code in the app. + */ public class UserSelectedWrongVerificationCodeException extends UserActionException { + + /** + * Constructs the exception with the default exception message. + */ public UserSelectedWrongVerificationCodeException() { super("User selected wrong verification code"); } diff --git a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java index 1079e602..4a1c2fd3 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java @@ -40,10 +40,16 @@ public class SessionStatusPoller { private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); + private final SmartIdConnector connector; private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; private long pollingSleepTimeout = 1L; + /** + * Constructs a new SessionStatusPoller with the specified SmartIdConnector. + * + * @param connector the SmartIdConnector to use for querying session status. + */ public SessionStatusPoller(SmartIdConnector connector) { this.connector = connector; } diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 7d4fb503..37ad21a8 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -33,10 +33,11 @@ import ee.sk.smartid.exception.SessionNotFoundException; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; @@ -47,8 +48,12 @@ import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +/** + * SmartIdConnector is the main interface for interacting with the Smart-ID service. + * It provides methods to initiate various types of sessions (authentication, signature, certificate choice) + * and to query session status and certificates. + */ public interface SmartIdConnector extends Serializable { /** @@ -79,7 +84,8 @@ public interface SmartIdConnector extends Serializable { /** * Initiates a linked notification based signature session. * - * @param request LinkedSignatureSessionRequest containing necessary parameters + * @param request LinkedSignatureSessionRequest containing necessary parameters + * @param documentNumber The document number to be used for the session * @return LinkedSignatureSessionResponse containing sessionID */ LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index 1cf64e6c..af11880e 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -45,10 +45,11 @@ import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.CertificateResponse; import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; @@ -60,7 +61,6 @@ import ee.sk.smartid.rest.dao.SemanticsIdentifier; import ee.sk.smartid.rest.dao.SessionStatus; import ee.sk.smartid.rest.dao.SessionStatusRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.ForbiddenException; @@ -75,6 +75,9 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriBuilder; +/** + * Smart-ID REST connector implementation. + */ public class SmartIdRestConnector implements SmartIdConnector { @Serial @@ -111,12 +114,23 @@ public class SmartIdRestConnector implements SmartIdConnector { private long sessionStatusResponseSocketOpenTimeValue; private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - public SmartIdRestConnector(String endpointUrl) { - this.endpointUrl = endpointUrl; + /** + * Creates a new instance of SmartIdRestConnector. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + */ + public SmartIdRestConnector(String baseUrl) { + this.endpointUrl = baseUrl; } - public SmartIdRestConnector(String endpointUrl, Client configuredClient) { - this(endpointUrl); + /** + * Creates a new instance of SmartIdRestConnector with a pre-configured client. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + * @param configuredClient a pre-configured client instace + */ + public SmartIdRestConnector(String baseUrl, Client configuredClient) { + this(baseUrl); this.configuredClient = configuredClient; } @@ -278,6 +292,12 @@ public void setSslContext(SSLContext sslContext) { this.sslContext = sslContext; } + /** + * Prepare client for the request. + * + * @param uri the target URI + * @return prepared invocation builder + */ protected Invocation.Builder prepareClient(URI uri) { Client client; if (this.configuredClient == null) { @@ -301,15 +321,30 @@ protected Invocation.Builder prepareClient(URI uri) { .header("User-Agent", buildUserAgentString()); } + /** + * Build user-agent string. + * + * @return user-agent string in the format: smart-id-java-client/[client-version] (Java/[java-version]) + */ protected String buildUserAgentString() { return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; } + /** + * Get client version from package implementation version. + * + * @return client version or "-" + */ protected String getClientVersion() { String clientVersion = getClass().getPackage().getImplementationVersion(); return clientVersion == null ? "-" : clientVersion; } + /** + * Get JDK major version. + * + * @return JDK major version or "-" + */ protected String getJdkMajorVersion() { try { return System.getProperty("java.version").split("_")[0]; diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java index 929568d2..443d2177 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java @@ -30,6 +30,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; +/** + * Request for querying certificate by document number. + * + * @param relyingPartyUUID Required. The relying party UUID + * @param relyingPartyName Required. The relying party name + * @param certificateLevel The certificate level. Possible values are "QSCD", "QUALIFIED" and "ADVANCED". If not specified, defaults to "QUALIFIED" + */ public record CertificateByDocumentNumberRequest(String relyingPartyUUID, String relyingPartyName, @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java index 568215e1..55b68605 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,6 +30,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Certificate info + * + * @param value Required. The certificate data in Base64-encoded format. + * @param certificateLevel Required. The certificate level, e.g. "QUALIFIED" or "ADVANCED". + */ @JsonIgnoreProperties(ignoreUnknown = true) public record CertificateInfo(String value, String certificateLevel) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java index 0f9cf545..9bc32de6 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -30,6 +30,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Response of certificate queried by document number + * + * @param state Required. State of the certificate + * @param cert Required if state is OK. Certificate information {@link CertificateInfo} + */ @JsonIgnoreProperties(ignoreUnknown = true) public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { } diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index cf61e85a..55b0f039 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -39,9 +39,9 @@ * * @param sessionID Required. The unique identifier of the session. * @param sessionToken Required. The token of the session. - * @param sessionSecret Required. - * @param deviceLinkBase Required. Base URI for generating device link - * @param receivedAt Timestamp when the response was received + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link. + * @param receivedAt Timestamp when the response was received. */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -51,6 +51,16 @@ public record DeviceLinkSessionResponse(String sessionID, URI deviceLinkBase, Instant receivedAt) implements Serializable { + /** + * Initializes a new instance of the {@link DeviceLinkSessionResponse} class. + *

+ * The receivedAt value is set to the current time. + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link + */ @JsonCreator public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, @JsonProperty("sessionToken") String sessionToken, diff --git a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java index f1faadf5..ec8b1886 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java @@ -28,43 +28,111 @@ import java.io.Serializable; +/** + * Representation of Semantic Identifier. + */ public class SemanticsIdentifier implements Serializable { - private final String identifier; + private final String identifier; + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCode the country code (e.g., EE, LT, LV). See {@link CountryCode} + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { + this.identifier = "" + identityType + countryCode + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code string and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { + this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type string, country code string and identity number. + * + * @param identityTypeString the identity type as string (e.g., PAS, IDC, PNO) + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { + this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identifier string. + * + * @param identifier the full semantics identifier string (e.g., "PAS EE-1234567890") + */ + public SemanticsIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Gets the full semantics identifier string. + * + * @return the full semantics identifier string + */ + public String getIdentifier() { + return identifier; + } + + /** + * 3-character identity type codes for SemanticsIdentifier + */ + public enum IdentityType { - public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { - this.identifier = "" + identityType + countryCode + "-" + identityNumber; - } + /** + * PAS - Passport + */ + PAS, - public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { - this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; - } + /** + * IDC - Identity Card + */ + IDC, - public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { - this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; - } + /** + * PNO - Personal Number + */ + PNO + } - public SemanticsIdentifier(String identifier) { - this.identifier = identifier; - } + /** + * 2-character country codes for SemanticsIdentifier + */ + public enum CountryCode { - public String getIdentifier() { - return identifier; - } + /** + * Estonia + */ + EE, - public enum IdentityType { - PAS, IDC, PNO - } + /** + * Lithuania + */ + LT, - public enum CountryCode { - EE, LT, LV - } + /** + * Latvia + */ + LV + } - @Override - public String toString() { - return "SemanticsIdentifier{" + - "identifier='" + identifier + '\'' + - '}'; - } + @Override + public String toString() { + return "SemanticsIdentifier{" + + "identifier='" + identifier + '\'' + + '}'; + } } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java index 70cc0f27..1a3accf1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,27 +26,54 @@ * #L% */ -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Certificate data in session status response. + *

+ * value - the certificate data in Base64-encoded format + * certificateLevel - the certificate level. Possible values: QUALIFIED or ADVANCED + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionCertificate implements Serializable { + private String value; private String certificateLevel; + /** + * Get the certificate value. + * + * @return the certificate data in Base64-encoded format + */ public String getValue() { return value; } + /** + * Set the certificate value. + * + * @param value the certificate data in Base64-encoded format + */ public void setValue(String value) { this.value = value; } + /** + * Gets the certificate level. + * + * @return the certificate level + */ public String getCertificateLevel() { return certificateLevel; } + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + */ public void setCertificateLevel(String certificateLevel) { this.certificateLevel = certificateLevel; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java index 72c0cb36..241378f5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,24 +30,50 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Mask generation algorithm data in session status response. + *

+ * algorithm - Required. The algorithm name, e.g. "id-mgf1" + * parameters - Required. The mask generation algorithm parameters + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionMaskGenAlgorithm implements Serializable { private String algorithm; private SessionMaskGenAlgorithmParameters parameters; + /** + * Gets the algorithm. + * + * @return the algorithm + */ public String getAlgorithm() { return algorithm; } + /** + * Sets the algorithm. + * + * @param algorithm the algorithm + */ public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } + /** + * Gets the parameters. + * + * @return the parameters + */ public SessionMaskGenAlgorithmParameters getParameters() { return parameters; } + /** + * Sets the parameters. + * + * @param parameters the parameters + */ public void setParameters(SessionMaskGenAlgorithmParameters parameters) { this.parameters = parameters; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java index 72eaac4b..029782ba 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,20 +26,34 @@ * #L% */ - import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Mask generation algorithm parameters. + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionMaskGenAlgorithmParameters implements Serializable { private String hashAlgorithm; + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ public String getHashAlgorithm() { return hashAlgorithm; } + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java index 3d638c4f..f7c2ca6b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,6 +30,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Represents how session ended - successfully, cancelled by user, timed out, etc. + * Available when session state is COMPLETE. + *

+ * endResult - Required. Reason for the session state being COMPLETED. + * documentNumber - Required. User's document number + * details - Additional details if user refused interaction. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionResult implements Serializable { @@ -37,26 +45,56 @@ public class SessionResult implements Serializable { private String documentNumber; private SessionResultDetails details; + /** + * Get exact end result of the session. + * + * @return end result of the session + */ public String getEndResult() { return endResult; } + /** + * Set end result of the session + * + * @param endResult end result of the session + */ public void setEndResult(String endResult) { this.endResult = endResult; } + /** + * Get document number of the user used in the session. + * + * @return document number of the user + */ public String getDocumentNumber() { return documentNumber; } + /** + * Set document number of the user + * + * @param documentNumber document number of the user + */ public void setDocumentNumber(String documentNumber) { this.documentNumber = documentNumber; } + /** + * Get additional details + * + * @return details of the session result + */ public SessionResultDetails getDetails() { return details; } + /** + * Set details of the session result + * + * @param details details of the session result + */ public void setDetails(SessionResultDetails details) { this.details = details; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java index a2fc032b..26e82578 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,15 +30,32 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Represents additional info when end result if user refused interactions. + *

+ * Required when end result is USER_REFUSED_INTERACTION. + *

+ * interaction - Type of the interaction that was cancelled by the user, e.g. "displayTextAndPIN" + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionResultDetails implements Serializable { private String interaction; + /** + * Gets type of the interaction that was cancelled by the user. + * + * @return type of the interaction that was cancelled by the user. + */ public String getInteraction() { return interaction; } + /** + * Sets type of the interaction type + * + * @param interaction type of the interaction + */ public void setInteraction(String interaction) { this.interaction = interaction; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java index 4d98d9d8..8bd48aad 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,6 +30,16 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Signature data. + *

+ * value - Required. Signature value in Base64-encoded format. + * serverRandom - Required. Server random value in Base64-encoded format. + * userChallenge - User challenge value in URL-safe Base64-encoded format. + * flowType - Required. The flow type, e.g. "QR", "Web2App". + * signatureAlgorithm - Required. The signature algorithm, e.g. "rsassa-pss". + * signatureAlgorithmParameters - Required. The signature algorithm parameters. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionSignature implements Serializable { @@ -40,51 +50,110 @@ public class SessionSignature implements Serializable { private String signatureAlgorithm; private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; + /** + * Get the signature value. + * + * @return the signature value + */ public String getValue() { return value; } + /** + * Set the signature value. + * + * @param value the signature value + */ public void setValue(String value) { this.value = value; } + /** + * Get the server random value. + * + * @return the server random value + */ public String getServerRandom() { return serverRandom; } + /** + * Set the server random value. + * + * @param serverRandom the server random value + */ public void setServerRandom(String serverRandom) { this.serverRandom = serverRandom; } + /** + * Get the user challenge value. + * + * @return the user challenge value + */ public String getUserChallenge() { return userChallenge; } + /** + * Set the user challenge value. + * + * @param userChallenge the user challenge value + */ public void setUserChallenge(String userChallenge) { this.userChallenge = userChallenge; } + /** + * Get the flow type. + * + * @return the flow type + */ public String getFlowType() { return flowType; } + /** + * Set the flow type. + * + * @param flowType the flow type + */ public void setFlowType(String flowType) { this.flowType = flowType; } + /** + * Get the signature algorithm. + * + * @return the signature algorithm + */ public String getSignatureAlgorithm() { return signatureAlgorithm; } + /** + * Set the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + */ public void setSignatureAlgorithm(String signatureAlgorithm) { this.signatureAlgorithm = signatureAlgorithm; } - + /** + * Get the signature algorithm parameters. + * + * @return the signature algorithm parameters + */ public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { return signatureAlgorithmParameters; } + /** + * Set the signature algorithm parameters. + * + * @param signatureAlgorithmParameters the signature algorithm parameters + */ public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { this.signatureAlgorithmParameters = signatureAlgorithmParameters; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java index 2251f428..5fbcee83 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,6 +30,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Signature algorithm parameters + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + * maskGenAlgorithm - Required. The mask generation algorithm + * saltLength - Required. The salt length, e.g. 32 for SHA-256 + * trailerField - Required. The trailer field, e.g. "0xbc"> + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionSignatureAlgorithmParameters implements Serializable { @@ -38,34 +46,74 @@ public class SessionSignatureAlgorithmParameters implements Serializable { private Integer saltLength; private String trailerField; + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ public String getHashAlgorithm() { return hashAlgorithm; } + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } + /** + * Gets mask generation algorithm. + * + * @return mask generation algorithm + */ public SessionMaskGenAlgorithm getMaskGenAlgorithm() { return maskGenAlgorithm; } + /** + * Sets mask generation algorithm. + * + * @param maskGenAlgorithm mask generation algorithm + */ public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { this.maskGenAlgorithm = maskGenAlgorithm; } + /** + * Gets salt length. + * + * @return salt length + */ public Integer getSaltLength() { return saltLength; } + /** + * Sets salt length. + * + * @param saltLength salt length + */ public void setSaltLength(Integer saltLength) { this.saltLength = saltLength; } + /** + * Gets trailer field. + * + * @return trailer field + */ public String getTrailerField() { return trailerField; } + /** + * Sets trailer field. + * + * @param trailerField trailer field + */ public void setTrailerField(String trailerField) { this.trailerField = trailerField; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 7fdfb52b..59641acc 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,6 +30,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +/** + * Represents response for active session query. + *

+ * state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE"> + * result - Required if state is "COMPLETE". Details about how session ended. + * signatureProtocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE". + * signature - Required if end result is OK. Signature data containing the actual signature and related information. + * cert - Required if end result is OK. Signer's certificate data. + * ignoredProperties - properties that were ignored from the session request. + * interactionTypeUsed - Required if end result is OK. Interaction type that was used in the session. + * deviceIpAddress - IP address of the device used in the session. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class SessionStatus implements Serializable { @@ -42,66 +54,146 @@ public class SessionStatus implements Serializable { private String interactionTypeUsed; private String deviceIpAddress; + /** + * Get state of the session + * + * @return state of the session + */ public String getState() { return state; } + /** + * Set state of the session + * + * @param state state of the session + */ public void setState(String state) { this.state = state; } + /** + * Get result of the session + * + * @return result of the session + */ public SessionResult getResult() { return result; } + /** + * Set result of the session + * + * @param result result of the session + */ public void setResult(SessionResult result) { this.result = result; } + /** + * Get signature protocol used + * + * @return signature protocol used + */ public String getSignatureProtocol() { return signatureProtocol; } + /** + * Sets the signature protocol used + * + * @param signatureProtocol signature protocol used + */ public void setSignatureProtocol(String signatureProtocol) { this.signatureProtocol = signatureProtocol; } + /** + * Get signature of the session + * + * @return signature of the session + */ public SessionSignature getSignature() { return signature; } + /** + * Set signature of the session + * + * @param signature signature of the session + */ public void setSignature(SessionSignature signature) { this.signature = signature; } + /** + * Get certificate of the session + * + * @return certificate of the session + */ public SessionCertificate getCert() { return cert; } + /** + * Set certificate of the session + * + * @param cert certificate of the session + */ public void setCert(SessionCertificate cert) { this.cert = cert; } + /** + * Get ignored properties provided in the session request. + * + * @return ignored properties + */ public String[] getIgnoredProperties() { return ignoredProperties; } + /** + * Set ignored properties provided in the session request. + * + * @param ignoredProperties ignored properties + */ public void setIgnoredProperties(String[] ignoredProperties) { this.ignoredProperties = ignoredProperties; } + /** + * Gets the interaction type used in the session + * + * @return the interaction type used in session + */ public String getInteractionTypeUsed() { return interactionTypeUsed; } + /** + * Sets the interaction type used in the session + * + * @param interactionTypeUsed the interaction type used in session + */ public void setInteractionTypeUsed(String interactionTypeUsed) { this.interactionTypeUsed = interactionTypeUsed; } + /** + * Gets the IP address of the device used in the session + * + * @return the device IP address + */ public String getDeviceIpAddress() { return deviceIpAddress; } + /** + * Sets the IP address of the device used in the session + * + * @param deviceIpAddress the device IP address + */ public void setDeviceIpAddress(String deviceIpAddress) { this.deviceIpAddress = deviceIpAddress; } diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java index b3286e61..faa46c19 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 SK ID Solutions AS + * Copyright (C) 2018 - 2025 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -29,45 +29,77 @@ import java.io.Serializable; import java.util.concurrent.TimeUnit; +/** + * Represents request to query session status. + *

+ * sessionId - the session ID to query status for. + * responseSocketOpenTimeUnit - time unit of how much time a network request socket should be kept open. + * responseSocketOpenTimeValue - time value of how much time a network request socket should be kept opn. + */ public class SessionStatusRequest implements Serializable { - private final String sessionId; - private TimeUnit responseSocketOpenTimeUnit; - private long responseSocketOpenTimeValue; + private final String sessionId; + private TimeUnit responseSocketOpenTimeUnit; + private long responseSocketOpenTimeValue; - public SessionStatusRequest(String sessionId) { - this.sessionId = sessionId; - } + /** + * Constructs a new SessionStatusRequest with the specified session ID. + * + * @param sessionId the session ID to query status for. + */ + public SessionStatusRequest(String sessionId) { + this.sessionId = sessionId; + } - public String getSessionId() { - return sessionId; - } + /** + * Gets the session ID. + * + * @return the session ID. + */ + public String getSessionId() { + return sessionId; + } - /** - * Request long poll timeout value. If not provided, a default is used. - *

- * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires - * set by this parameter. - *

- * Caller can tune the request parameters inside the bounds set by service operator. - * - * @param timeUnit time unit of how much time a network request socket should be kept open. - * @param timeValue time value of how much time a network request socket should be kept open. - */ - public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - responseSocketOpenTimeUnit = timeUnit; - responseSocketOpenTimeValue = timeValue; - } + /** + * Request long poll timeout value. If not provided, a default is used. + *

+ * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires + * set by this parameter. + *

+ * Caller can tune the request parameters inside the bounds set by service operator. + * + * @param timeUnit time unit of how much time a network request socket should be kept open. + * @param timeValue time value of how much time a network request socket should be kept open. + */ + public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + responseSocketOpenTimeUnit = timeUnit; + responseSocketOpenTimeValue = timeValue; + } - public boolean isResponseSocketOpenTimeSet() { - return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; - } + /** + * Gets whether response socket open time is set. + * + * @return true if response socket open time is set, false otherwise. + */ + public boolean isResponseSocketOpenTimeSet() { + return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; + } - public TimeUnit getResponseSocketOpenTimeUnit() { - return responseSocketOpenTimeUnit; - } + /** + * Gets response socket open time unit. + * + * @return response socket open time unit. + */ + public TimeUnit getResponseSocketOpenTimeUnit() { + return responseSocketOpenTimeUnit; + } - public long getResponseSocketOpenTimeValue() { - return responseSocketOpenTimeValue; - } + /** + * Gets response socket open time value. + * + * @return response socket open time value. + */ + public long getResponseSocketOpenTimeValue() { + return responseSocketOpenTimeValue; + } } diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index bc903f55..c946a88c 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -60,6 +60,9 @@ import ee.sk.smartid.AuthenticationIdentity; import ee.sk.smartid.exception.permanent.SmartIdClientException; +/** + * Utility class for extracting attributes from X.509 certificates. + */ public final class CertificateAttributeUtil { private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index e150b0b2..64ac36e7 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -12,10 +12,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,11 +26,6 @@ * #L% */ -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -38,7 +33,17 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Utility class for handling national identity numbers (personal codes). + */ public class NationalIdentityNumberUtil { + private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); private static final DateTimeFormatter DATE_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuuMMdd") @@ -48,19 +53,19 @@ public class NationalIdentityNumberUtil { * Detect date-of-birth from a Baltic national identification number if possible or return null. *

* This method always returns the value for all Estonian and Lithuanian national identification numbers. - * + *

* It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 * (starting with "32") do not carry date-of-birth. - * + *

* For non-Baltic countries (countries other than Estonia, Latvia or Lithuania) it always returns null * (even if it would be possible to deduce date of birth from national identity number). - * + *

* Newer (but not all) Smart-ID certificates have date-of-birth on a separate attribute. * It is recommended to use that value if present. - * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) * * @param authenticationIdentity Authentication identity * @return DateOfBirth or null if it cannot be detected from personal code + * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) */ public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { String identityNumber = authenticationIdentity.getIdentityNumber(); @@ -72,6 +77,13 @@ public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIden }; } + /** + * Parses date of birth from Estonian or Lithuanian national identity number. + * + * @param eeOrLtNationalIdentityNumber Estonian or Lithuanian national identity number + * @return Date of birth + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); @@ -89,6 +101,15 @@ public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber } } + /** + * Parses date of birth from Latvian national identity number if possible. + *

+ * Latvian personal codes issued after July 1st 2017 (starting with "32") do not carry date-of-birth and null is returned. + * + * @param lvNationalIdentityNumber Latvian national identity number + * @return Date of birth or null if the personal code does not carry birthdate info + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { String birthDay = lvNationalIdentityNumber.substring(0, 2); if (isNonParsableLVPersonCodePrefix(birthDay)) { diff --git a/src/test/java/ee/sk/smartid/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java index 6e5fad02..dc96b6c7 100644 --- a/src/test/java/ee/sk/smartid/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -34,6 +34,9 @@ public final class CertificateUtil { + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + private CertificateUtil() { } @@ -57,14 +60,13 @@ public static X509Certificate toX509CertificateFromEncodedString(String base64Ce } public static String getEncodedCertificateData(String certificate) { - return certificate.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") + return certificate.replace(BEGIN_CERTIFICATE, "") + .replace(END_CERTIFICATE, "") .replace("\n", ""); } private static byte[] getX509CertificateBytes(String encodedData) { - String certificate = CertificateParser.BEGIN_CERT + "\n" + encodedData + "\n" + CertificateParser.END_CERT; + String certificate = BEGIN_CERTIFICATE + "\n" + encodedData + "\n" + END_CERTIFICATE; return certificate.getBytes(StandardCharsets.UTF_8); } - } From 37ce2cf8ec341065056f33448857e612a3272d0f Mon Sep 17 00:00:00 2001 From: Kimmo Date: Tue, 14 Oct 2025 20:15:44 +0300 Subject: [PATCH 56/57] Github workflow updates --- .github/workflows/check.yaml | 80 +- .github/workflows/publish.yaml | 128 +- .github/workflows/tests.yaml | 72 +- .gitignore | 12 +- .mvn/wrapper/MavenWrapperDownloader.java | 234 +- .mvn/wrapper/maven-wrapper.properties | 36 +- CHANGELOG.md | 650 +-- DEVELOPERS.md | 42 +- LICENSE | 42 +- LICENSE.3RD-PARTY | 228 +- MIGRATION_GUIDE.md | 150 +- README.md | 3072 +++++++------- mvnw | 632 +-- mvnw.cmd | 376 +- pom.xml | 600 +-- src/license/LICENSE.EPL-1.0 | 408 +- src/license/third-party-file-template.ftl | 42 +- .../AuthenticationCertificateLevel.java | 142 +- .../ee/sk/smartid/AuthenticationIdentity.java | 370 +- .../smartid/AuthenticationIdentityMapper.java | 136 +- .../ee/sk/smartid/AuthenticationResponse.java | 534 +-- .../smartid/AuthenticationResponseMapper.java | 94 +- .../AuthenticationResponseMapperImpl.java | 556 +-- ...ificateByDocumentNumberRequestBuilder.java | 380 +- .../CertificateByDocumentNumberResult.java | 76 +- .../sk/smartid/CertificateChoiceResponse.java | 280 +- .../CertificateChoiceResponseValidator.java | 336 +- .../java/ee/sk/smartid/CertificateLevel.java | 154 +- .../java/ee/sk/smartid/CertificateParser.java | 146 +- .../java/ee/sk/smartid/CertificateState.java | 114 +- .../ee/sk/smartid/CertificateValidator.java | 100 +- .../sk/smartid/CertificateValidatorImpl.java | 212 +- .../sk/smartid/DefaultTrustedCACertStore.java | 172 +- .../smartid/DefaultTrustedCAStoreBuilder.java | 316 +- ...ceLinkAuthenticationResponseValidator.java | 430 +- ...nkAuthenticationSessionRequestBuilder.java | 722 ++-- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 722 ++-- ...ertificateChoiceSessionRequestBuilder.java | 424 +- ...iceLinkSignatureSessionRequestBuilder.java | 710 ++-- .../java/ee/sk/smartid/DeviceLinkType.java | 126 +- .../java/ee/sk/smartid/DigestCalculator.java | 122 +- src/main/java/ee/sk/smartid/DigestInput.java | 104 +- .../ee/sk/smartid/ErrorResultHandler.java | 194 +- .../sk/smartid/FileTrustedCAStoreBuilder.java | 448 +-- src/main/java/ee/sk/smartid/FlowType.java | 194 +- .../java/ee/sk/smartid/HashAlgorithm.java | 204 +- ...icationSignatureSessionRequestBuilder.java | 560 +-- .../java/ee/sk/smartid/MaskGenAlgorithm.java | 160 +- ...dSignatureCertificatePurposeValidator.java | 112 +- ...cationAuthenticationResponseValidator.java | 376 +- ...onAuthenticationSessionRequestBuilder.java | 630 +-- ...ertificateChoiceSessionRequestBuilder.java | 390 +- ...icationSignatureSessionRequestBuilder.java | 676 ++-- .../java/ee/sk/smartid/QrCodeGenerator.java | 284 +- ...dSignatureCertificatePurposeValidator.java | 256 +- src/main/java/ee/sk/smartid/RpChallenge.java | 110 +- .../ee/sk/smartid/RpChallengeGenerator.java | 148 +- .../ee/sk/smartid/RsaSsaPssParameters.java | 280 +- src/main/java/ee/sk/smartid/SessionType.java | 122 +- src/main/java/ee/sk/smartid/SignableData.java | 186 +- src/main/java/ee/sk/smartid/SignableHash.java | 174 +- .../ee/sk/smartid/SignatureAlgorithm.java | 164 +- .../SignatureCertificatePurposeValidator.java | 92 +- ...ureCertificatePurposeValidatorFactory.java | 82 +- ...ertificatePurposeValidatorFactoryImpl.java | 102 +- .../java/ee/sk/smartid/SignatureProtocol.java | 86 +- .../java/ee/sk/smartid/SignatureResponse.java | 546 +-- .../smartid/SignatureResponseValidator.java | 680 ++-- .../sk/smartid/SignatureValueValidator.java | 102 +- .../smartid/SignatureValueValidatorImpl.java | 208 +- .../java/ee/sk/smartid/SmartIdClient.java | 824 ++-- src/main/java/ee/sk/smartid/TrailerField.java | 162 +- .../ee/sk/smartid/TrustedCACertStore.java | 118 +- .../smartid/VerificationCodeCalculator.java | 130 +- .../ee/sk/smartid/VerificationCodeType.java | 100 +- ...enticationCertificatePurposeValidator.java | 92 +- ...ionCertificatePurposeValidatorFactory.java | 86 +- ...ertificatePurposeValidatorFactoryImpl.java | 100 +- ...enticationCertificatePurposeValidator.java | 110 +- ...enticationCertificatePurposeValidator.java | 154 +- .../ee/sk/smartid/common/InteractionType.java | 94 +- .../smartid/common/InteractionValidator.java | 110 +- .../sk/smartid/common/InteractionsMapper.java | 124 +- .../sk/smartid/common/SmartIdInteraction.java | 108 +- ...nQualifiedSmartIdCertificateValidator.java | 144 +- ...tIdAuthenticationCertificateValidator.java | 226 +- .../common/devicelink/CallbackUrl.java | 76 +- .../devicelink/UrlSafeTokenGenerator.java | 170 +- .../interactions/DeviceLinkInteraction.java | 174 +- .../DeviceLinkInteractionType.java | 124 +- .../interactions/NotificationInteraction.java | 196 +- .../NotificationInteractionType.java | 132 +- .../exception/EnduringSmartIdException.java | 110 +- .../exception/SessionNotFoundException.java | 66 +- .../SessionSecretMismatchException.java | 84 +- .../smartid/exception/SmartIdException.java | 110 +- ...UnprocessableSmartIdResponseException.java | 110 +- .../exception/UserAccountException.java | 88 +- .../exception/UserActionException.java | 86 +- .../ExpectedLinkedSessionException.java | 88 +- .../permanent/ProtocolFailureException.java | 88 +- ...ingPartyAccountConfigurationException.java | 92 +- .../permanent/ServerMaintenanceException.java | 84 +- .../permanent/SmartIdClientException.java | 108 +- .../SmartIdRequestSetupException.java | 108 +- .../permanent/SmartIdServerException.java | 84 +- .../CertificateLevelMismatchException.java | 102 +- .../DocumentUnusableException.java | 80 +- ...eAccountOfRequestedTypeFoundException.java | 92 +- ...ersonShouldViewSmartIdPortalException.java | 102 +- ...InteractionNotSupportedByAppException.java | 86 +- .../UserAccountNotFoundException.java | 84 +- .../UserAccountUnusableException.java | 84 +- .../useraction/SessionTimeoutException.java | 84 +- .../UserRefusedCertChoiceException.java | 82 +- ...erRefusedConfirmationMessageException.java | 82 +- ...essageWithVerificationChoiceException.java | 82 +- ...UserRefusedDisplayTextAndPinException.java | 82 +- .../useraction/UserRefusedException.java | 104 +- ...electedWrongVerificationCodeException.java | 86 +- .../ee/sk/smartid/rest/LoggingFilter.java | 316 +- .../sk/smartid/rest/SessionStatusPoller.java | 218 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 394 +- .../sk/smartid/rest/SmartIdRestConnector.java | 820 ++-- .../AcspV2SignatureProtocolParameters.java | 82 +- .../CertificateByDocumentNumberRequest.java | 86 +- .../sk/smartid/rest/dao/CertificateInfo.java | 82 +- .../smartid/rest/dao/CertificateResponse.java | 82 +- ...eviceLinkAuthenticationSessionRequest.java | 112 +- ...ceLinkCertificateChoiceSessionRequest.java | 106 +- .../rest/dao/DeviceLinkSessionResponse.java | 142 +- .../DeviceLinkSignatureSessionRequest.java | 114 +- .../ee/sk/smartid/rest/dao/Interaction.java | 82 +- .../dao/LinkedSignatureSessionRequest.java | 114 +- .../dao/LinkedSignatureSessionResponse.java | 76 +- ...ificationAuthenticationSessionRequest.java | 112 +- ...ficationAuthenticationSessionResponse.java | 82 +- ...cationCertificateChoiceSessionRequest.java | 104 +- ...ationCertificateChoiceSessionResponse.java | 80 +- .../NotificationSignatureSessionRequest.java | 110 +- .../NotificationSignatureSessionResponse.java | 84 +- .../RawDigestSignatureProtocolParameters.java | 80 +- .../smartid/rest/dao/RequestProperties.java | 78 +- .../smartid/rest/dao/SemanticsIdentifier.java | 276 +- .../smartid/rest/dao/SessionCertificate.java | 160 +- .../rest/dao/SessionMaskGenAlgorithm.java | 160 +- .../SessionMaskGenAlgorithmParameters.java | 120 +- .../ee/sk/smartid/rest/dao/SessionResult.java | 202 +- .../rest/dao/SessionResultDetails.java | 124 +- .../sk/smartid/rest/dao/SessionSignature.java | 320 +- .../SessionSignatureAlgorithmParameters.java | 240 +- .../ee/sk/smartid/rest/dao/SessionStatus.java | 400 +- .../rest/dao/SessionStatusRequest.java | 210 +- .../dao/SignatureAlgorithmParameters.java | 76 +- .../sk/smartid/rest/dao/VerificationCode.java | 82 +- .../ee/sk/smartid/util/CallbackUrlUtil.java | 182 +- .../util/CertificateAttributeUtil.java | 406 +- .../ee/sk/smartid/util/InteractionUtil.java | 176 +- .../util/NationalIdentityNumberUtil.java | 284 +- src/main/java/ee/sk/smartid/util/SetUtil.java | 110 +- .../java/ee/sk/smartid/util/StringUtil.java | 132 +- .../trusted_certificates/EID-SK_2016.pem.crt | 78 +- .../trusted_certificates/NQ-SK_2016.pem.crt | 74 +- .../AuthenticationIdentityMapperTest.java | 108 +- .../smartid/AuthenticationIdentityTest.java | 104 +- .../AuthenticationResponseMapperImplTest.java | 1674 ++++---- .../smartid/CapabilitiesArgumentProvider.java | 100 +- ...ateByDocumentNumberRequestBuilderTest.java | 580 +-- ...ertificateChoiceResponseValidatorTest.java | 580 +-- .../ee/sk/smartid/CertificateParserTest.java | 82 +- .../java/ee/sk/smartid/CertificateUtil.java | 144 +- .../smartid/CertificateValidatorImplTest.java | 154 +- .../sk/smartid/ClientRequestHeaderFilter.java | 102 +- .../DefaultTrustedCAStoreBuilderTest.java | 136 +- ...nkAuthenticationResponseValidatorTest.java | 534 +-- ...thenticationSessionRequestBuilderTest.java | 962 ++--- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 1100 ++--- ...ficateChoiceSessionRequestBuilderTest.java | 556 +-- ...inkSignatureSessionRequestBuilderTest.java | 1044 ++--- .../ee/sk/smartid/DigestCalculatorTest.java | 158 +- ...plicateDeviceLinkInteractionsProvider.java | 100 +- ...tificationInteractionArgumentProvider.java | 98 +- .../ee/sk/smartid/ErrorResultHandlerTest.java | 262 +- .../FileDefaultTrustedCAStoreBuilderTest.java | 204 +- src/test/java/ee/sk/smartid/FileUtil.java | 110 +- .../smartid/InvalidCertificateGenerator.java | 292 +- .../InvalidRpChallengeArgumentProvider.java | 100 +- ...ionSignatureSessionRequestBuilderTest.java | 516 +-- ...natureCertificatePurposeValidatorTest.java | 240 +- ...onAuthenticationResponseValidatorTest.java | 408 +- ...thenticationSessionRequestBuilderTest.java | 770 ++-- ...ficateChoiceSessionRequestBuilderTest.java | 538 +-- ...ionSignatureSessionRequestBuilderTest.java | 992 ++--- .../ee/sk/smartid/QrCodeGeneratorTest.java | 420 +- src/test/java/ee/sk/smartid/QrCodeUtil.java | 138 +- ...natureCertificatePurposeValidatorTest.java | 310 +- .../sk/smartid/RpChallengeGeneratorTest.java | 138 +- ...essionEndResultErrorArgumentsProvider.java | 130 +- .../java/ee/sk/smartid/SignableDataTest.java | 136 +- .../java/ee/sk/smartid/SignableHashTest.java | 128 +- .../SignatureResponseValidatorTest.java | 1174 +++--- .../SignatureValueValidatorImplTest.java | 242 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 1622 ++++---- .../ee/sk/smartid/SmartIdDemoCondition.java | 104 +- .../smartid/SmartIdDemoIntegrationTest.java | 80 +- .../sk/smartid/SmartIdRestServiceStubs.java | 306 +- ...erRefusedInteractionArgumentsProvider.java | 96 +- .../VerificationCodeCalculatorTest.java | 166 +- ...cationCertificatePurposeValidatorTest.java | 334 +- ...cationCertificatePurposeValidatorTest.java | 340 +- .../common/InteractionValidatorTest.java | 146 +- .../common/InteractionsMapperTest.java | 176 +- .../devicelink/UrlSafeTokenGeneratorTest.java | 154 +- .../DeviceLinkInteractionTest.java | 200 +- .../NotificationInteractionTest.java | 250 +- .../smartid/dao/SemanticsIdentifierTest.java | 120 +- .../integration/ReadmeIntegrationTest.java | 1898 ++++----- .../SmartIdRestIntegrationTest.java | 664 +-- .../smartid/rest/SessionStatusPollerTest.java | 166 +- .../rest/SmartIdRestConnectorTest.java | 3558 ++++++++--------- .../sk/smartid/util/CallbackUrlUtilTest.java | 210 +- .../util/CertificateAttributeUtilTest.java | 284 +- .../util/NationalIdentityNumberUtilTest.java | 282 +- src/test/resources/logback.xml | 30 +- ...ation-session-request-invalid-request.json | 12 +- ...uthentication-session-request-qr-code.json | 26 +- ...ession-request-same-device-all-fields.json | 34 +- ...uest-same-device-only-required-fields.json | 28 +- ...entication-session-request-all-fields.json | 36 +- ...uthentication-session-request-invalid.json | 8 +- ...-session-request-only-required-fields.json | 26 +- ...by-document-number-request-all-fields.json | 8 +- ...t-number-request-only-required-fields.json | 6 +- ...ice-link-signature-request-all-fields.json | 26 +- ...device-link-signature-request-qr-code.json | 24 +- ...ce-link-signature-request-same-device.json | 26 +- ...ate-choice-session-request-all-fields.json | 18 +- ...te-choice-session-request-device-link.json | 10 +- ...te-choice-session-request-for-qr-code.json | 8 +- ...-signature-session-request-all-fields.json | 36 +- ...-session-request-only-required-fields.json | 26 +- ...ate-choice-session-request-all-fields.json | 16 +- ...e-session-request-invalid-credentials.json | 6 +- ...ficate-choice-session-request-invalid.json | 4 +- ...-session-request-only-required-fields.json | 6 +- ...-signature-session-request-all-fields.json | 34 +- ...e-session-request-invalid-credentials.json | 24 +- ...ion-signature-session-request-invalid.json | 4 +- ...-session-request-only-required-fields.json | 24 +- ...-link-authentication-session-response.json | 12 +- .../notification-session-response.json | 6 +- ...ocument-number-response-unknown-state.json | 18 +- ...rtificate-by-document-number-response.json | 16 +- .../session-status-account-unusable.json | 14 +- .../session-status-document-unusable.json | 14 +- ...ession-status-expected-linked-session.json | 14 +- .../session-status-protocol-failure.json | 14 +- ...tatus-running-with-ignored-properties.json | 8 +- .../responses/session-status-running.json | 6 +- .../session-status-server-error.json | 14 +- ...sion-status-successful-authentication.json | 74 +- ...-status-successful-certificate-choice.json | 28 +- .../session-status-successful-signature.json | 74 +- .../responses/session-status-timeout.json | 14 +- ...ssion-status-user-refused-cert-choice.json | 14 +- ...s-user-refused-confirmation-vc-choice.json | 22 +- ...sion-status-user-refused-confirmation.json | 22 +- ...tus-user-refused-display-text-and-pin.json | 22 +- ...session-status-user-refused-vc-choice.json | 22 +- .../session-status-user-refused.json | 14 +- .../responses/session-status-wrong-vc.json | 14 +- ...evice-link-signature-session-response.json | 12 +- ...k-certificate-choice-session-response.json | 12 +- ...tification-signature-session-response.json | 6 +- ...n-certificate-choice-session-response.json | 6 +- ...tification-signature-session-response.json | 12 +- src/test/resources/sid_demo_sk_ee.pem | 78 +- .../test-certs/TEST_SK_ROOT_G1_2021E.pem.crt | 34 +- .../TEST_of_SK_OCSP_RESPONDER_2020.pem.cer | 56 +- .../auth-cert-40504040001-demo-q.crt | 76 +- .../test-certs/auth-cert-40504040001.pem.crt | 76 +- .../auth-pnolv-020100-29990-mock-q.crt | 92 +- src/test/resources/test-certs/ca-cert.pem.crt | 46 +- .../cert-choice-cert-40504040001.pem.cert | 84 +- .../resources/test-certs/expired-cert.pem.crt | 78 +- .../test-certs/nq-auth-cert-40504049999.crt | 76 +- .../resources/test-certs/nq-signing-cert.pem | 74 +- .../test-certs/other-auth-cert.pem.crt | 78 +- .../test-certs/sign-cert-40504040001.pem.crt | 84 +- .../TEST_EID-NQ_2021E.pem.crt | 44 +- ...EST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt | 46 +- 291 files changed, 32030 insertions(+), 32030 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 9e1f8b92..a31c170b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -1,40 +1,40 @@ -name: Run dependency and spotbugs checks - -on: - workflow_run: - workflows: ["Run tests"] - types: - - completed - workflow_dispatch: - -permissions: - contents: read - -jobs: - run-checks: - name: Run dependency and spotbugs checks - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Run dependency check - run: | - ./mvnw -DossIndexUsername=${{ secrets.ossIndexUsername }} -DossIndexPassword=${{ secrets.ossIndexPassword }} -DnvdApiKey=${{ secrets.nvdApiKey }} org.owasp:dependency-check-maven:check - - - name: Archive dependency report - uses: actions/upload-artifact@v4 - with: - name: dependency-report - path: target/dependency-check-report.html - - - name: Run spotbugs check - run: | - ./mvnw spotbugs:check +name: Run dependency and spotbugs checks + +on: + workflow_run: + workflows: ["Run tests"] + types: + - completed + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-checks: + name: Run dependency and spotbugs checks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Run dependency check + run: | + ./mvnw -DossIndexUsername=${{ secrets.ossIndexUsername }} -DossIndexPassword=${{ secrets.ossIndexPassword }} -DnvdApiKey=${{ secrets.nvdApiKey }} org.owasp:dependency-check-maven:check + + - name: Archive dependency report + uses: actions/upload-artifact@v4 + with: + name: dependency-report + path: target/dependency-check-report.html + + - name: Run spotbugs check + run: | + ./mvnw spotbugs:check diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 703134e3..b138f84d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,64 +1,64 @@ -name: Publish to maven repository - -on: - release: - types: - - published - -permissions: - contents: read - -jobs: - package_and_publish: - name: Publish to maven repository - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup java SDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - #passphrase: ${{ secrets.PASSPHRASE }} - - - name: Create bundle and upload to oss.sonatype.org (staging) - # Fail on first error - run: | - set -e - version=${{ github.event.release.name }} - artifact=smart-id-java-client-$version - echo "[INFO] Artifact name: $artifact" - ./mvnw versions:set -DnewVersion="$version" - ./mvnw package -DskipTests - cd target - rm -rf ee/sk/smartid/smart-id-java-client/$version - mkdir -p ee/sk/smartid/smart-id-java-client/$version - cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ - cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ - cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ - cp ../pom.xml ee/sk/smartid/smart-id-java-client/$version/$artifact.pom - cd ee/sk/smartid/smart-id-java-client/$version - gpg -ab $artifact.pom - gpg -ab $artifact.jar - gpg -ab $artifact-sources.jar - gpg -ab $artifact-javadoc.jar - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha256sum "$file" | cut -d " " -f 1 > "$file.sha256"; done' _ {} + - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha1sum "$file" | cut -d " " -f 1 > "$file.sha1"; done' _ {} + - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do md5sum "$file" | cut -d " " -f 1 > "$file.md5"; done' _ {} + - cd ../../../../../ - zip bundle.zip ee/sk/smartid/smart-id-java-client/$version/* - CODE=$(curl -w "%{http_code}" -o curl_response.txt -s --request POST --verbose --header 'Authorization: Bearer ${{ secrets.SONATYPETOKEN }}' --form bundle=@bundle.zip https://central.sonatype.com/api/v1/publisher/upload) - echo "[INFO] ------------------------------------------------------------------------" - echo "[INFO] Upload to central.sonatype.com ResponseCode: $CODE" - cat curl_response.txt - echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" - echo "[INFO] ------------------------------------------------------------------------" - [[ $CODE == 201 ]] && exit 0 || exit 1 - +name: Publish to maven repository + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + package_and_publish: + name: Publish to maven repository + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup java SDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - + name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + #passphrase: ${{ secrets.PASSPHRASE }} + + - name: Create bundle and upload to oss.sonatype.org (staging) + # Fail on first error + run: | + set -e + version=${{ github.event.release.name }} + artifact=smart-id-java-client-$version + echo "[INFO] Artifact name: $artifact" + ./mvnw versions:set -DnewVersion="$version" + ./mvnw package -DskipTests + cd target + rm -rf ee/sk/smartid/smart-id-java-client/$version + mkdir -p ee/sk/smartid/smart-id-java-client/$version + cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ + cp ../pom.xml ee/sk/smartid/smart-id-java-client/$version/$artifact.pom + cd ee/sk/smartid/smart-id-java-client/$version + gpg -ab $artifact.pom + gpg -ab $artifact.jar + gpg -ab $artifact-sources.jar + gpg -ab $artifact-javadoc.jar + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha256sum "$file" | cut -d " " -f 1 > "$file.sha256"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha1sum "$file" | cut -d " " -f 1 > "$file.sha1"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do md5sum "$file" | cut -d " " -f 1 > "$file.md5"; done' _ {} + + cd ../../../../../ + zip bundle.zip ee/sk/smartid/smart-id-java-client/$version/* + CODE=$(curl -w "%{http_code}" -o curl_response.txt -s --request POST --verbose --header 'Authorization: Bearer ${{ secrets.SONATYPETOKEN }}' --form bundle=@bundle.zip https://central.sonatype.com/api/v1/publisher/upload) + echo "[INFO] ------------------------------------------------------------------------" + echo "[INFO] Upload to central.sonatype.com ResponseCode: $CODE" + cat curl_response.txt + echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" + echo "[INFO] ------------------------------------------------------------------------" + [[ $CODE == 201 ]] && exit 0 || exit 1 + diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f0598fd2..bfd4fd91 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,37 +1,37 @@ -name: Run tests - -on: - push: - branches: [ "master", "v3.1" ] - pull_request: - branches: [ "master" ] - -permissions: - contents: read - -jobs: - run-tests: - runs-on: ubuntu-latest - strategy: - matrix: - java-version: ['17', '21'] - name: Run tests with java SDK ${{ matrix.java-version }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup java - uses: actions/setup-java@v4 - with: - java-version: ${{ matrix.java-version }} - distribution: 'temurin' - cache: maven - - - name: Check JAVA version (v${{ matrix.java-version }}) - run: java -version - - - name: Run tests - # Fail on first error - run: | - set -e +name: Run tests + +on: + push: + branches: [ "master", "v3.1" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: ['17', '21'] + name: Run tests with java SDK ${{ matrix.java-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + + - name: Check JAVA version (v${{ matrix.java-version }}) + run: java -version + + - name: Run tests + # Fail on first error + run: | + set -e mvn test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 91fb933f..39b4f2c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -.idea -*.iml -target -.settings -.classpath -.project +.idea +*.iml +target +.settings +.classpath +.project diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java index b901097f..519f798c 100644 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -1,117 +1,117 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 8c79a83a..0169a252 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,18 +1,18 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index 31e9571b..880c2a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,325 +1,325 @@ -# Changelog - -All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## [3.1-?] - TBD - -### Structural changes - -- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. -- Removed all Smart-ID v2 related classes, tests, and documentation. -- Updated README to reflect removal of v2-related information. - -### Dynamic-link auth to device-link auth changes - -- Renamed dynamic-link authentication to device-link authentication. -- Updated authentication endpoints to use /device-link/ paths. -- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). -- Replaced signature algorithm list with fixed `rsassa-pss`. -- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. -- Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. -- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. -- Introduced a `DeviceLinkBuilder` to generate device links. - - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. - - Ensures `elapsedSeconds` is only used for QR_CODE flows. - - Moved `deviceLinkBase` to required input (no more default). - - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. - - New payload structure includes required and optional fields as per documentation. - - `schemeName` is now configurable (default is `"smart-id"`). - - Does not store `sessionSecret`, ensures it must be passed to the build method. -- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. - -- Updates to session status response - - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. - - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling - - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` - - Renamed `interactionFlowUsed` to `interactionTypeUsed`. -- Updated exception message of `DocumentUnusableException` -- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response -- Updated AuthenticationSessionRequest and related classes to records. -- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. - - Created to builder-classes for loading trusted CA certificates - - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore - - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates -- Update AuthenticationResponseValidator to DeviceLinkAuthenticationResponseValidator - - update signature value validation - - added additional certificate validations (validate certificate chain and certificate purpose) - - added validation for userChallenge and userChallengeVerifier in case of same device flows - - added validators QualifiedAuthenticationCertificatePurposeValidator and NonQualifiedAuthenticationCertificatePurposeValidator to validate - certificate purpose based on requested certificate level. - -- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest - -### Added handling for querying certificate by document number - -- Added new endpoint: `POST /v3/signature/certificate/{document-number}`. -- Added new builder CertificateByDocumentNumberRequestBuilder to create the request -- Add new request objects CertificateByDocumentNumberRequest and response CertificateResponse -- Removed notification-based certificate choice request with document number. - -### Updated dynamic-link signature to device-link signature - -- Renamed dynamic-link signature to device-link signature. -- Updated signature endpoints to use /device-link/ paths. -- Replaced signature algorithm list with fixed `rsassa-pss`. -- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. -- Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Removed HashType and update SignableHash and SignableData to use HashAlgorithm -- Update signature session-status validations - - Signature - - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. - - Allowed `flowType`: QR · App2App · Web2App · Notification. - - Fixed `signatureAlgorithm` to `rsassa-pss`. - - `signatureAlgorithmParameters` - - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. - - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. - - `saltLength`: 32 / 48 / 64 bytes to match chosen hash algorithm octet length. - - `trailerField`: `0xbc`. - - - Certificate - - Must be a Smart-ID *signature* certificate: - - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or - `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. - - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. - - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. - -- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and - `SignatureResponseValidator`. - -## Update dynamic-link certificate choice to device-link certificate choice - -- Renamed dynamic-link certificate choice to device-link certificate choice. -- Updated certificate choice endpoint to use /device-link/ paths. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Updated CertificateChoiceResponseMapper - - Renamed to CertificateChoiceResponseValidator - - Added CertificateValidator as dependency - -## Added linked signature session support - -- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. -- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. -- Added request LinkedSignatureSessionRequest and LinkedSignatureSessionResponse. - -### Updated notification-based authentication to work with Smart-ID API v3.1 - -- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 -- Removed verificationCodeChoice interactions and related handling -- Removed AuthenticationHash. -- Added NotificationAuthenticationResponseValidator - -### Updated notification-based certificate choice to work with Smart-ID API v3.1 - -- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint -- Added NotificationCertificateChoiceSessionRequest - -### Updated notification-based signature to work with Smart-ID API v3.1 - -- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint -- Added NotificationSignatureSessionRequest - -## [3.0] - 2023-10-14 - -### Added -- Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 - package. - - New builder classes to start v3 sessions: - - DynamicLinkAuthenticationSessionRequestBuilder - - DynamicLinkCertificateChoiceSessionRequestBuilder - - DynamicLinkSignatureSessionRequestBuilder - - NotificationAuthenticationSessionRequestBuilder - - NotificationCertificateChoiceSessionRequestBuilder - - NotificationSignatureSessionRequestBuilder - - Helper class for dynamic link - - AuthCode - used for generating authCode necessary for dynamic-link - - QrCodeGenerator - to create QR-code from dynamic-link - - DynamicContentBuilder - to create dynamic link or QR-code - - Support for sessions status request handling for the v3 path. - - Added AuthenticationResponseMapper for validating required fields and mapping session status to authentication response - - Added AuthenticationResponseValidator to validate certificate and signed authentication response and construct AuthenticationIdentity - - Added SignatureResponseMapper for validating required fields and mapping session status to signature response - - Added CertificateChoiceResponseMapper for validating required fields and mapping session status to certificate choice response - -### Changed -- Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. -- Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` -- Typo fixes, code cleanup and improvements -- Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. - -### Removed -- Removed deprecated methods from AuthenticationIdentity - -### Java and dependency updates -- Updated minimal supported java to version 17 -- Updated slf4j-api to version 2.0.16 -- Updated jackson dependencies to version 2.17.2 -- Added jakarta.ws.rs:jakarta.ws.rs-api -- Updated jersey dependencies to version 3.1.8 -- Updated bouncy-castle artifact to bcprov-jdk18on on version 1.78.1 -- Updated jaxb-runtime to version 4.0.5 - -## [2.3] - 2023-05-06 -- To request the IP address of the device running Smart-ID app, the following methods were added: - - AuthenticationRequestBuilder.withShareMdClientIpAddress(boolean) - - CertificateRequestBuilder.withShareMdClientIpAddress(boolean) - - SignatureRequestBuilder.withShareMdClientIpAddress(boolean) -- The IP address returned can be read out using: - - SmartIdAuthenticationResponse.getDeviceIpAddress() - - SmartIdCertificate.getDeviceIpAddress() - - SmartIdSignature.getDeviceIpAddress() - -## [2.2.2] - 2022-11-14 - -### Changed -- upgrade jackson, jersey and dependency-check-maven plugin -### Documented -- How to extract date-of-birth from a certificate added as a separate paragraph to readme. -- Added two tests into SmartIdIntegrationTest that demonstrate fetching and parsing a certificate with date-of-birth -- Changed demo SSL certificate -- add correct way of adding trusted certificates in Readme [#73](https://github.com/SK-EID/smart-id-java-client/issues/73) - -## [2.2.1] - 2022-09-12 - -### Fixed -- added jakarta.ws.rs:jakarta.ws.rs-api as a dependency to avoid ClassNotFoundException with spring framework - -### Changed -- Updated dependencies - -### Changes in tests and documentation -- How to use a proxy server - added documentation to README.md and tests to ReadmeTest.java - -## [2.2] - 2022-02-22 - -### Changed -- Reduced number of external dependencies by removing commons-lang3, commons-io, commons-codec. - -### Added -- [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java#:~:text=getDeviceIpAddress()) -- [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java#:~:text=getDeviceIpAddress()) -- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) - -## [2.1.4] - 2022-01-14 - -### Fixed -- bug where non-Baltic certificates without date-of-birth resulted with an exception - -## [2.1.3] - 2021-12-22 - -### Fixed -- Possible NPE fix (in rare cases under load testing the SessionStatus is null) - -### Changes in tests -- Changed document number in tests -- Added a flag (SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO) to switch off tests that make requests to Smart-ID demo env. - -## [2.1.2] - 2021-11-03 - -### Changed -- AuthenticationResponseValidator.constructAuthenticationIdentity() converted into a static method - -## [2.1.1] - 2021-09-06 - -### Fixed -- Bug fixed in parsing date of birth for Latvian ID-codes. - -## [2.1] - 2021-07-07 - -### Added -- AuthenticationIdentity.getDateOfBirth() to get person birthdate (if available). -- Add library version number and Java major release number to User-Agent header of outgoing requests - -## [2.0] - 2020-11-20 - -### Changed -- Switch to Smart-ID API 2.0 -- `AuthenticationResponseValidator.validate()` returns AuthenticationIdentity if validation passes. - If validation fails then `SmartIdResponseValidationException` or its subclass `CertificateLevelMismatchException` (if signer's certificate is below requested level) is thrown. -- Grouped exceptions thrown by library to reduce need to handle each exception individually. See Readme.md for detail info. -- Minimum Java level raised to Java 8 -- Relying Party must keep a list of trusted certificates (in plain text or in a trust store). -- request.setVcChoice() was removed in Smart-ID API 2.0 and replaced by request.setAllowedInteractionsOrder(); - - -### Added -- New parameter `allowedInteractionsOrder` added to authentication and signing requests. It replaces parameters displayText and requestProperties.vcChoice -- New parameter `interactionFlowUsed` added into session status response message. -- If user refuses then a dedicated exception is thrown that indicates exact screen where user pressed cancel. Thrown exception is subclass of `UserRefusedException`. - -### Removed -- all endpoints using `NationalIdentityNumber` are now removed as this functionality has been removed from Smart-ID API 2.0 -- errors that the caller cannot recover from are now removed from method throws list. -- Hard-coded certificates were removed together with methods: - - SmartIdClient.useDemoEnvSSLCertificates() - - SmartIdClient.useLiveEnvSSLCertificates() - -## [1.6] - 2020-05-25 - -### Added -- UserSelectedWrongVerificationCodeException is now thrown when user selects wrong verification code from three-choice selection. - -## [1.5.1] - 2020-05-18 -### Security -- Bumped jackson-databind from 2.9.10.1 to 2.9.10.4 -- Updated Maven Dependency Check plugin version. - -### Changed -- AuthenticationRequestBuilder method withRequestProperties access modifier changed to public - -### Added - -- Maven wrapper to project - -## [1.5] - 2019-11-12 -### Security -- CVE-2019-16943 -- CVE-2019-17531 -- CVE-2019-16942 -- CVE-2019-16335 -- CVE-2019-14540 -### Added -- SSL pinning to verify, that the client is communicating with SK environment [#3](https://github.com/SK-EID/smart-id-java-client/issues/3) -- SmartIdClient.addTrustedSSLCertificates(String ...sslCertificate) - add ssl certificates when Sk starts to use new certs -- SmartIdClient.setTrustedSSLCertificates(String ...sslCertificates) - set specific ssl certificates to trust -- SmartIdClient.useDemoEnvSSLCertificates() - uses only demo env ssl certificates -- SmartIdClient.useLiveEnvSSLCertificates() - uses only live env ssl certificates -- SmartIdClient.loadSslCertificatesFromKeystore(KeyStore keyStore) - loads only the certificates from keystore - -## [1.4] - 2019-09-23 -### Added -- Client configuration on different JAX-WS implementations. [#22](https://github.com/SK-EID/smart-id-java-client/issues/22), [#11](https://github.com/SK-EID/mid-rest-java-client/issues/11) -- SmartIdClient.setConfiguredClient() -- SmartIdClient.setNetworkConnectionConfig() - -## [1.3] - 2019-09-13 -### Added -- Capabilities parameter ([#25](https://github.com/SK-EID/smart-id-java-client/pull/25)) -- [Request properties](https://github.com/SK-EID/smart-id-documentation#416-request-properties) (vcChoice) for authentication and signing ([#21](https://github.com/SK-EID/smart-id-java-client/pull/21)) - -## [1.2] - 2019-08-21 -### Added -- Support for [Semantics Identifier](https://github.com/SK-EID/smart-id-documentation#412-rest-object-references) ([#17](https://github.com/SK-EID/smart-id-java-client/pull/17)) -- Document number to authentication responses ([#14](https://github.com/SK-EID/smart-id-java-client/issues/14)) -- Maven dependency check plugin for continuous security -- SpotBugs plugin for continuous bug detection - -## [1.1] - 2018-12-10 - -### Added -- SmartIdClient.getSmartIdConnector() -- SmartIdRequestBuilder.validateSessionResult -- MIT license to code base - -### Changed -- renamed SignatureSessionResponse.sessionId -> SignatureSessionResponse.sessionID -- renamed SmartIdRestConnector -> SmartIdConnector -- renamed SessionStatus.getCertificate() -> SessionStatus.getCert() -- renamed SessionSignature.getValueInBase64() -> SessionSignature.getValue() -- improved and cleaned up tests +# Changelog + +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [3.1-?] - TBD + +### Structural changes + +- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. +- Removed all Smart-ID v2 related classes, tests, and documentation. +- Updated README to reflect removal of v2-related information. + +### Dynamic-link auth to device-link auth changes + +- Renamed dynamic-link authentication to device-link authentication. +- Updated authentication endpoints to use /device-link/ paths. +- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. +- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. +- Introduced a `DeviceLinkBuilder` to generate device links. + - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. + - Ensures `elapsedSeconds` is only used for QR_CODE flows. + - Moved `deviceLinkBase` to required input (no more default). + - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. + - New payload structure includes required and optional fields as per documentation. + - `schemeName` is now configurable (default is `"smart-id"`). + - Does not store `sessionSecret`, ensures it must be passed to the build method. +- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. + +- Updates to session status response + - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. + - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling + - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` + - Renamed `interactionFlowUsed` to `interactionTypeUsed`. +- Updated exception message of `DocumentUnusableException` +- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response +- Updated AuthenticationSessionRequest and related classes to records. +- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. + - Created to builder-classes for loading trusted CA certificates + - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore + - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates +- Update AuthenticationResponseValidator to DeviceLinkAuthenticationResponseValidator + - update signature value validation + - added additional certificate validations (validate certificate chain and certificate purpose) + - added validation for userChallenge and userChallengeVerifier in case of same device flows + - added validators QualifiedAuthenticationCertificatePurposeValidator and NonQualifiedAuthenticationCertificatePurposeValidator to validate + certificate purpose based on requested certificate level. + +- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest + +### Added handling for querying certificate by document number + +- Added new endpoint: `POST /v3/signature/certificate/{document-number}`. +- Added new builder CertificateByDocumentNumberRequestBuilder to create the request +- Add new request objects CertificateByDocumentNumberRequest and response CertificateResponse +- Removed notification-based certificate choice request with document number. + +### Updated dynamic-link signature to device-link signature + +- Renamed dynamic-link signature to device-link signature. +- Updated signature endpoints to use /device-link/ paths. +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Removed HashType and update SignableHash and SignableData to use HashAlgorithm +- Update signature session-status validations + - Signature + - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. + - Allowed `flowType`: QR · App2App · Web2App · Notification. + - Fixed `signatureAlgorithm` to `rsassa-pss`. + - `signatureAlgorithmParameters` + - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. + - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. + - `saltLength`: 32 / 48 / 64 bytes to match chosen hash algorithm octet length. + - `trailerField`: `0xbc`. + + - Certificate + - Must be a Smart-ID *signature* certificate: + - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or + `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. + - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. + - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. + +- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and + `SignatureResponseValidator`. + +## Update dynamic-link certificate choice to device-link certificate choice + +- Renamed dynamic-link certificate choice to device-link certificate choice. +- Updated certificate choice endpoint to use /device-link/ paths. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Updated CertificateChoiceResponseMapper + - Renamed to CertificateChoiceResponseValidator + - Added CertificateValidator as dependency + +## Added linked signature session support + +- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. +- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. +- Added request LinkedSignatureSessionRequest and LinkedSignatureSessionResponse. + +### Updated notification-based authentication to work with Smart-ID API v3.1 + +- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 +- Removed verificationCodeChoice interactions and related handling +- Removed AuthenticationHash. +- Added NotificationAuthenticationResponseValidator + +### Updated notification-based certificate choice to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint +- Added NotificationCertificateChoiceSessionRequest + +### Updated notification-based signature to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint +- Added NotificationSignatureSessionRequest + +## [3.0] - 2023-10-14 + +### Added +- Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 + package. + - New builder classes to start v3 sessions: + - DynamicLinkAuthenticationSessionRequestBuilder + - DynamicLinkCertificateChoiceSessionRequestBuilder + - DynamicLinkSignatureSessionRequestBuilder + - NotificationAuthenticationSessionRequestBuilder + - NotificationCertificateChoiceSessionRequestBuilder + - NotificationSignatureSessionRequestBuilder + - Helper class for dynamic link + - AuthCode - used for generating authCode necessary for dynamic-link + - QrCodeGenerator - to create QR-code from dynamic-link + - DynamicContentBuilder - to create dynamic link or QR-code + - Support for sessions status request handling for the v3 path. + - Added AuthenticationResponseMapper for validating required fields and mapping session status to authentication response + - Added AuthenticationResponseValidator to validate certificate and signed authentication response and construct AuthenticationIdentity + - Added SignatureResponseMapper for validating required fields and mapping session status to signature response + - Added CertificateChoiceResponseMapper for validating required fields and mapping session status to certificate choice response + +### Changed +- Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. +- Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` +- Typo fixes, code cleanup and improvements +- Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. + +### Removed +- Removed deprecated methods from AuthenticationIdentity + +### Java and dependency updates +- Updated minimal supported java to version 17 +- Updated slf4j-api to version 2.0.16 +- Updated jackson dependencies to version 2.17.2 +- Added jakarta.ws.rs:jakarta.ws.rs-api +- Updated jersey dependencies to version 3.1.8 +- Updated bouncy-castle artifact to bcprov-jdk18on on version 1.78.1 +- Updated jaxb-runtime to version 4.0.5 + +## [2.3] - 2023-05-06 +- To request the IP address of the device running Smart-ID app, the following methods were added: + - AuthenticationRequestBuilder.withShareMdClientIpAddress(boolean) + - CertificateRequestBuilder.withShareMdClientIpAddress(boolean) + - SignatureRequestBuilder.withShareMdClientIpAddress(boolean) +- The IP address returned can be read out using: + - SmartIdAuthenticationResponse.getDeviceIpAddress() + - SmartIdCertificate.getDeviceIpAddress() + - SmartIdSignature.getDeviceIpAddress() + +## [2.2.2] - 2022-11-14 + +### Changed +- upgrade jackson, jersey and dependency-check-maven plugin +### Documented +- How to extract date-of-birth from a certificate added as a separate paragraph to readme. +- Added two tests into SmartIdIntegrationTest that demonstrate fetching and parsing a certificate with date-of-birth +- Changed demo SSL certificate +- add correct way of adding trusted certificates in Readme [#73](https://github.com/SK-EID/smart-id-java-client/issues/73) + +## [2.2.1] - 2022-09-12 + +### Fixed +- added jakarta.ws.rs:jakarta.ws.rs-api as a dependency to avoid ClassNotFoundException with spring framework + +### Changed +- Updated dependencies + +### Changes in tests and documentation +- How to use a proxy server - added documentation to README.md and tests to ReadmeTest.java + +## [2.2] - 2022-02-22 + +### Changed +- Reduced number of external dependencies by removing commons-lang3, commons-io, commons-codec. + +### Added +- [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java#:~:text=getDeviceIpAddress()) +- [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java#:~:text=getDeviceIpAddress()) +- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) + +## [2.1.4] - 2022-01-14 + +### Fixed +- bug where non-Baltic certificates without date-of-birth resulted with an exception + +## [2.1.3] - 2021-12-22 + +### Fixed +- Possible NPE fix (in rare cases under load testing the SessionStatus is null) + +### Changes in tests +- Changed document number in tests +- Added a flag (SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO) to switch off tests that make requests to Smart-ID demo env. + +## [2.1.2] - 2021-11-03 + +### Changed +- AuthenticationResponseValidator.constructAuthenticationIdentity() converted into a static method + +## [2.1.1] - 2021-09-06 + +### Fixed +- Bug fixed in parsing date of birth for Latvian ID-codes. + +## [2.1] - 2021-07-07 + +### Added +- AuthenticationIdentity.getDateOfBirth() to get person birthdate (if available). +- Add library version number and Java major release number to User-Agent header of outgoing requests + +## [2.0] - 2020-11-20 + +### Changed +- Switch to Smart-ID API 2.0 +- `AuthenticationResponseValidator.validate()` returns AuthenticationIdentity if validation passes. + If validation fails then `SmartIdResponseValidationException` or its subclass `CertificateLevelMismatchException` (if signer's certificate is below requested level) is thrown. +- Grouped exceptions thrown by library to reduce need to handle each exception individually. See Readme.md for detail info. +- Minimum Java level raised to Java 8 +- Relying Party must keep a list of trusted certificates (in plain text or in a trust store). +- request.setVcChoice() was removed in Smart-ID API 2.0 and replaced by request.setAllowedInteractionsOrder(); + + +### Added +- New parameter `allowedInteractionsOrder` added to authentication and signing requests. It replaces parameters displayText and requestProperties.vcChoice +- New parameter `interactionFlowUsed` added into session status response message. +- If user refuses then a dedicated exception is thrown that indicates exact screen where user pressed cancel. Thrown exception is subclass of `UserRefusedException`. + +### Removed +- all endpoints using `NationalIdentityNumber` are now removed as this functionality has been removed from Smart-ID API 2.0 +- errors that the caller cannot recover from are now removed from method throws list. +- Hard-coded certificates were removed together with methods: + - SmartIdClient.useDemoEnvSSLCertificates() + - SmartIdClient.useLiveEnvSSLCertificates() + +## [1.6] - 2020-05-25 + +### Added +- UserSelectedWrongVerificationCodeException is now thrown when user selects wrong verification code from three-choice selection. + +## [1.5.1] - 2020-05-18 +### Security +- Bumped jackson-databind from 2.9.10.1 to 2.9.10.4 +- Updated Maven Dependency Check plugin version. + +### Changed +- AuthenticationRequestBuilder method withRequestProperties access modifier changed to public + +### Added + +- Maven wrapper to project + +## [1.5] - 2019-11-12 +### Security +- CVE-2019-16943 +- CVE-2019-17531 +- CVE-2019-16942 +- CVE-2019-16335 +- CVE-2019-14540 +### Added +- SSL pinning to verify, that the client is communicating with SK environment [#3](https://github.com/SK-EID/smart-id-java-client/issues/3) +- SmartIdClient.addTrustedSSLCertificates(String ...sslCertificate) - add ssl certificates when Sk starts to use new certs +- SmartIdClient.setTrustedSSLCertificates(String ...sslCertificates) - set specific ssl certificates to trust +- SmartIdClient.useDemoEnvSSLCertificates() - uses only demo env ssl certificates +- SmartIdClient.useLiveEnvSSLCertificates() - uses only live env ssl certificates +- SmartIdClient.loadSslCertificatesFromKeystore(KeyStore keyStore) - loads only the certificates from keystore + +## [1.4] - 2019-09-23 +### Added +- Client configuration on different JAX-WS implementations. [#22](https://github.com/SK-EID/smart-id-java-client/issues/22), [#11](https://github.com/SK-EID/mid-rest-java-client/issues/11) +- SmartIdClient.setConfiguredClient() +- SmartIdClient.setNetworkConnectionConfig() + +## [1.3] - 2019-09-13 +### Added +- Capabilities parameter ([#25](https://github.com/SK-EID/smart-id-java-client/pull/25)) +- [Request properties](https://github.com/SK-EID/smart-id-documentation#416-request-properties) (vcChoice) for authentication and signing ([#21](https://github.com/SK-EID/smart-id-java-client/pull/21)) + +## [1.2] - 2019-08-21 +### Added +- Support for [Semantics Identifier](https://github.com/SK-EID/smart-id-documentation#412-rest-object-references) ([#17](https://github.com/SK-EID/smart-id-java-client/pull/17)) +- Document number to authentication responses ([#14](https://github.com/SK-EID/smart-id-java-client/issues/14)) +- Maven dependency check plugin for continuous security +- SpotBugs plugin for continuous bug detection + +## [1.1] - 2018-12-10 + +### Added +- SmartIdClient.getSmartIdConnector() +- SmartIdRequestBuilder.validateSessionResult +- MIT license to code base + +### Changed +- renamed SignatureSessionResponse.sessionId -> SignatureSessionResponse.sessionID +- renamed SmartIdRestConnector -> SmartIdConnector +- renamed SessionStatus.getCertificate() -> SessionStatus.getCert() +- renamed SessionSignature.getValueInBase64() -> SessionSignature.getValue() +- improved and cleaned up tests diff --git a/DEVELOPERS.md b/DEVELOPERS.md index b7b18c47..7ff3a10f 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -1,22 +1,22 @@ -# Guidelines for developers of this library - -## Main design principles - -### Use as few external dependencies as possible - -Always prefer the functionality built into Java in favor of Apache Commons/Lang/Codec etc. - -You can check the effective dependency tree: -mvn dependency:tree -Dscope=compile - -### Add a license header for each file - -mvn license:update-file-header - -### Keep 3rd party licenses file updated - -If you add or remove dependencies then don't forget to run - -mvn license:add-third-party - +# Guidelines for developers of this library + +## Main design principles + +### Use as few external dependencies as possible + +Always prefer the functionality built into Java in favor of Apache Commons/Lang/Codec etc. + +You can check the effective dependency tree: +mvn dependency:tree -Dscope=compile + +### Add a license header for each file + +mvn license:update-file-header + +### Keep 3rd party licenses file updated + +If you add or remove dependencies then don't forget to run + +mvn license:add-third-party + to update the file: LICENSE.3RD-PARTY \ No newline at end of file diff --git a/LICENSE b/LICENSE index ed9097ea..38b50810 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License - -Copyright (c) 2018 SK ID Solutions AS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License + +Copyright (c) 2018 SK ID Solutions AS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index 4e788d46..f9a2bee8 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,114 +1,114 @@ -List of 112 third-party dependencies (auto-generated on 2025-05-19 with License Maven Plugin): - -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) -* (Apache License, Version 2.0) jcommander (com.beust:jcommander:1.82 - https://jcommander.org) -* (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) -* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) -* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.17.1 - https://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1 - https://github.com/FasterXML/jackson-dataformats-text) -* (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) -* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) -* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) -* (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) -* (BSD 3-clause License w/nuclear disclaimer) Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) -* (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.3.1 - https://github.com/jknack/handlebars.java/handlebars) -* (The Apache Software License, Version 2.0) Handlebars Helpers (com.github.jknack:handlebars-helpers:4.3.1 - https://github.com/jknack/handlebars.java/handlebars-helpers) -* (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.26.1 - https://errorprone.info/error_prone_annotations) -* (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess) -* (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) -* (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) -* (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) -* (The Apache Software License, Version 2.0) ZXing Core (com.google.zxing:core:3.5.3 - https://github.com/zxing/zxing/core) -* (The Apache Software License, Version 2.0) ZXing Java SE extensions (com.google.zxing:javase:3.5.3 - https://github.com/zxing/zxing/javase) -* (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) -* (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) -* (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) -* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) -* (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.16.1 - https://commons.apache.org/proper/commons-codec/) -* (Apache-2.0) Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) -* (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.11.0 - https://commons.apache.org/proper/commons-io/) -* (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.1 - https://commons.apache.org/proper/commons-logging/) -* (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) -* (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) -* (The Apache Software License, Version 2.0) Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) -* (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) -* (EPL-2.0) (GPL-2.0-with-classpath-exception) Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:4.0.0 - https://github.com/jakartaee/rest/jakarta.ws.rs-api) -* (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) -* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.15.0 - https://bytebuddy.net/byte-buddy) -* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.15.0 - https://bytebuddy.net/byte-buddy-agent) -* (The Apache Software License, Version 2.0) json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.40.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) -* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) -* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) -* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) -* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) -* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) -* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) -* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) -* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) -* (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) -* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) -* (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/) -* (EDL 1.0) Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:11.0.20 - https://eclipse.dev/jetty/jetty-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:11.0.20 - https://eclipse.dev/jetty/jetty-http) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:11.0.20 - https://eclipse.dev/jetty/jetty-io) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:11.0.20 - https://eclipse.dev/jetty/jetty-proxy) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Security (org.eclipse.jetty:jetty-security:11.0.20 - https://eclipse.dev/jetty/jetty-security) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:11.0.20 - https://eclipse.dev/jetty/jetty-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:11.0.20 - https://eclipse.dev/jetty/jetty-servlet) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:11.0.20 - https://eclipse.dev/jetty/jetty-servlets) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:11.0.20 - https://eclipse.dev/jetty/jetty-util) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:11.0.20 - https://eclipse.dev/jetty/jetty-webapp) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:11.0.20 - https://eclipse.dev/jetty/jetty-xml) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-common) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-hpack) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api) -* (EPL 2.0) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) -* (EPL 2.0) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) -* (EPL 2.0) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) -* (EPL 2.0) (GPL2 w/ CPE) OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) -* (EPL 2.0) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) -* (Eclipse Distribution License - v 1.0) JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-apache-connector) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) -* (Apache License, 2.0) (EPL 2.0) (Public Domain) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) -* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) -* (BSD-3-Clause) Hamcrest (org.hamcrest:hamcrest:3.0 - http://hamcrest.org/JavaHamcrest/) -* (BSD-3-Clause) Hamcrest Core (org.hamcrest:hamcrest-core:3.0 - http://hamcrest.org/JavaHamcrest/) -* (BSD-3-Clause) Hamcrest Library (org.hamcrest:hamcrest-library:3.0 - http://hamcrest.org/JavaHamcrest/) -* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) -* (Apache License, Version 2.0) Java Annotation Indexer (org.jboss:jandex:2.4.5.Final - http://www.jboss.org/jandex) -* (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.3.Final - http://www.jboss.org) -* (Apache License 2.0) RESTEasy Client (org.jboss.resteasy:resteasy-client:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Client API (org.jboss.resteasy:resteasy-client-api:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Core (org.jboss.resteasy:resteasy-core:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Core SPI (org.jboss.resteasy:resteasy-core-spi:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Jackson 2 Provider (org.jboss.resteasy:resteasy-jackson2-provider:6.2.10.Final - https://resteasy.dev) -* (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.11.0 - https://junit.org/junit5/) -* (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.11.0 - https://junit.org/junit5/) -* (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.0 - https://junit.org/junit5/) -* (MIT) mockito-core (org.mockito:mockito-core:5.13.0 - https://github.com/mockito/mockito) -* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis) -* (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) -* (MIT-0) reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) -* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:2.0.16 - http://www.slf4j.org) -* (The Apache Software License, Version 2.0) WireMock (org.wiremock:wiremock:3.9.1 - http://wiremock.org) -* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) -* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.10.0 - https://www.xmlunit.org/) -* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.10.0 - https://www.xmlunit.org/xmlunit-placeholders/) -* (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) +List of 112 third-party dependencies (auto-generated on 2025-05-19 with License Maven Plugin): + +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) +* (Apache License, Version 2.0) jcommander (com.beust:jcommander:1.82 - https://jcommander.org) +* (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) +* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) +* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.17.1 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1 - https://github.com/FasterXML/jackson-dataformats-text) +* (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) +* (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) +* (BSD 3-clause License w/nuclear disclaimer) Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) +* (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.3.1 - https://github.com/jknack/handlebars.java/handlebars) +* (The Apache Software License, Version 2.0) Handlebars Helpers (com.github.jknack:handlebars-helpers:4.3.1 - https://github.com/jknack/handlebars.java/handlebars-helpers) +* (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.26.1 - https://errorprone.info/error_prone_annotations) +* (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess) +* (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) +* (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) +* (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) +* (The Apache Software License, Version 2.0) ZXing Core (com.google.zxing:core:3.5.3 - https://github.com/zxing/zxing/core) +* (The Apache Software License, Version 2.0) ZXing Java SE extensions (com.google.zxing:javase:3.5.3 - https://github.com/zxing/zxing/javase) +* (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) +* (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) +* (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) +* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) +* (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.16.1 - https://commons.apache.org/proper/commons-codec/) +* (Apache-2.0) Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) +* (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.11.0 - https://commons.apache.org/proper/commons-io/) +* (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.1 - https://commons.apache.org/proper/commons-logging/) +* (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) +* (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) +* (The Apache Software License, Version 2.0) Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) +* (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) +* (EPL-2.0) (GPL-2.0-with-classpath-exception) Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:4.0.0 - https://github.com/jakartaee/rest/jakarta.ws.rs-api) +* (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) +* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.15.0 - https://bytebuddy.net/byte-buddy) +* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.15.0 - https://bytebuddy.net/byte-buddy-agent) +* (The Apache Software License, Version 2.0) json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.40.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) +* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) +* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) +* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) +* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) +* (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) +* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) +* (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/) +* (EDL 1.0) Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:11.0.20 - https://eclipse.dev/jetty/jetty-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:11.0.20 - https://eclipse.dev/jetty/jetty-http) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:11.0.20 - https://eclipse.dev/jetty/jetty-io) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:11.0.20 - https://eclipse.dev/jetty/jetty-proxy) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Security (org.eclipse.jetty:jetty-security:11.0.20 - https://eclipse.dev/jetty/jetty-security) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:11.0.20 - https://eclipse.dev/jetty/jetty-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:11.0.20 - https://eclipse.dev/jetty/jetty-servlet) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:11.0.20 - https://eclipse.dev/jetty/jetty-servlets) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:11.0.20 - https://eclipse.dev/jetty/jetty-util) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:11.0.20 - https://eclipse.dev/jetty/jetty-webapp) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:11.0.20 - https://eclipse.dev/jetty/jetty-xml) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-common) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-hpack) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api) +* (EPL 2.0) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) +* (EPL 2.0) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) +* (EPL 2.0) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) +* (EPL 2.0) (GPL2 w/ CPE) OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) +* (EPL 2.0) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) +* (Eclipse Distribution License - v 1.0) JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-apache-connector) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) +* (Apache License, 2.0) (EPL 2.0) (Public Domain) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) +* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) +* (BSD-3-Clause) Hamcrest (org.hamcrest:hamcrest:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Core (org.hamcrest:hamcrest-core:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Library (org.hamcrest:hamcrest-library:3.0 - http://hamcrest.org/JavaHamcrest/) +* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) +* (Apache License, Version 2.0) Java Annotation Indexer (org.jboss:jandex:2.4.5.Final - http://www.jboss.org/jandex) +* (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.3.Final - http://www.jboss.org) +* (Apache License 2.0) RESTEasy Client (org.jboss.resteasy:resteasy-client:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Client API (org.jboss.resteasy:resteasy-client-api:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core (org.jboss.resteasy:resteasy-core:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core SPI (org.jboss.resteasy:resteasy-core-spi:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Jackson 2 Provider (org.jboss.resteasy:resteasy-jackson2-provider:6.2.10.Final - https://resteasy.dev) +* (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.0 - https://junit.org/junit5/) +* (MIT) mockito-core (org.mockito:mockito-core:5.13.0 - https://github.com/mockito/mockito) +* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis) +* (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) +* (MIT-0) reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) +* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:2.0.16 - http://www.slf4j.org) +* (The Apache Software License, Version 2.0) WireMock (org.wiremock:wiremock:3.9.1 - http://wiremock.org) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) +* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.10.0 - https://www.xmlunit.org/) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.10.0 - https://www.xmlunit.org/xmlunit-placeholders/) +* (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 67bf42f2..d8e4d9f8 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,75 +1,75 @@ -# Intro - -Library v3.1 supports only Smart-ID v3 API. -All the previous v2 related code has been removed and all the code necessary for Smart-ID API v3 is under package smartid. -Some classes could also be used in v3 and for those classes the package did not change. - -# Migrating from Smart-ID v2 to Smart-ID v3 API - -## Migrating authentication - -Smart-ID v3 authentication offers new methods to construct builders `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create authentication session request builders. -It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. - -### Overview of V2 authentication flow - -1. Create authentication hash -2. Generate verification code from authentication hash -3. Verification code can be shown to the user -4. Create builder and set values. -5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. -6. After session status is `COMPLETE` response will be checked in the build method. -7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. - -### Moving to V3 authentication flow - -1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` -2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` -3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. - - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. -5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. -6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. - -## Migrating signing - -Signing migration will be focusing on moving to signature flow when device link authentication has been completed before. - -### Overview of V2 signing flow - -1. Set values for certificate choice builder and call build method. Should return certificate as a response. -2. Use queried certificate to create DataToSign object. Requires DigiDoc4j library. -3. Create SignableData from DataToSign. -4. Create verification code from SignableData -5. Create signature builder and set values. -6. Call build method (`sign()`) to create signing session and to start polling for session status. -7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. - -### Moving to V3 signing flow - with DigiDoc4j library - -DigiDoc4j library does not currently support signing with signature algorithm RSASSA-PSS. Support will be added in the future. -There is a possible workaround to use DigiDoc4j library and DSS library together to create ASICS container and sign it with Smart-ID v3 API. -Steps below include examples how to set up DataToSign for signing with RSASSA-PSS and how validate returned signature value. - -#### Steps to migrate - -1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). -2. Use `SignableData` to create digested value for signing. Example for setting up DataToSign with DSS: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdDeviceLinkSignatureService.java#L181 -3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -5. Poll for session status until its complete. -6. Validate session response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. -7. Validate signature value. Example for validating signature value: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdSignatureService.java#L65 - -### Moving to V3 signing flow without DigiDoc4j library - -NB! Without DigiDoc4j library integrator has to provide implementation for creating signed container. -Smart-id-java-client only provides means to validate that signature response has required fields and returned signature value is valid. - -1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). -2. Use `SignableData` to create digested value for signing. -3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -5. Poll for session status until its complete. -6. Validate session status response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. -7. Validate signature value with `SignatureValueValidator` +# Intro + +Library v3.1 supports only Smart-ID v3 API. +All the previous v2 related code has been removed and all the code necessary for Smart-ID API v3 is under package smartid. +Some classes could also be used in v3 and for those classes the package did not change. + +# Migrating from Smart-ID v2 to Smart-ID v3 API + +## Migrating authentication + +Smart-ID v3 authentication offers new methods to construct builders `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create authentication session request builders. +It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. + +### Overview of V2 authentication flow + +1. Create authentication hash +2. Generate verification code from authentication hash +3. Verification code can be shown to the user +4. Create builder and set values. +5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. +6. After session status is `COMPLETE` response will be checked in the build method. +7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. + +### Moving to V3 authentication flow + +1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` +2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` +3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. + - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. +5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. +6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. + +## Migrating signing + +Signing migration will be focusing on moving to signature flow when device link authentication has been completed before. + +### Overview of V2 signing flow + +1. Set values for certificate choice builder and call build method. Should return certificate as a response. +2. Use queried certificate to create DataToSign object. Requires DigiDoc4j library. +3. Create SignableData from DataToSign. +4. Create verification code from SignableData +5. Create signature builder and set values. +6. Call build method (`sign()`) to create signing session and to start polling for session status. +7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. + +### Moving to V3 signing flow - with DigiDoc4j library + +DigiDoc4j library does not currently support signing with signature algorithm RSASSA-PSS. Support will be added in the future. +There is a possible workaround to use DigiDoc4j library and DSS library together to create ASICS container and sign it with Smart-ID v3 API. +Steps below include examples how to set up DataToSign for signing with RSASSA-PSS and how validate returned signature value. + +#### Steps to migrate + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. Example for setting up DataToSign with DSS: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdDeviceLinkSignatureService.java#L181 +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value. Example for validating signature value: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdSignatureService.java#L65 + +### Moving to V3 signing flow without DigiDoc4j library + +NB! Without DigiDoc4j library integrator has to provide implementation for creating signed container. +Smart-id-java-client only provides means to validate that signature response has required fields and returned signature value is valid. + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session status response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value with `SignatureValueValidator` diff --git a/README.md b/README.md index 80a7df8a..d51a2853 100644 --- a/README.md +++ b/README.md @@ -1,1537 +1,1537 @@ -[![Tests](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml/badge.svg)](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml) -[![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) -[![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) -[![License: MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) - -# Smart-ID Java client - -This library supports Smart-ID API v3.1. - -## Table of contents - -* [Smart-ID Java client](#smart-id-java-client) - * [Introduction](#introduction) - * [Features](#features) - * [Requirements](#requirements) - * [Getting the library](#getting-the-library) - * [Changelog](#changelog) -* [How to use it with RP API v3.1](#how-to-use-api-v31) - * [Test accounts for testing](#test-accounts-for-testing) - * [Logging](#logging) - * [Log request payloads](#log-request-payloads) - * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) - * [Device link flows](#device-link-flows) - * [Device link authentication session](#device-link-authentication-session) - * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) - * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) - * [Initiating a device link-based authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) - * [Initiating a device link-based authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) - * [Device-link signature session](#device-link-signature-session) - * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) - * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) - * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) - * [Examples of allowed device-link interaction](#examples-of-device-link-interactions) - * [Additional request properties](#additional-device-link-session-request-properties) - * [Generating QR-code or device link](#generating-qr-code-or-device-link) - * [Generating device link ](#generating-device-link) - * [Device link parameters](#device-link-parameters) - * [Overriding default values](#overriding-default-values) - * [Generating QR-code](#generating-qr-code) - * [Generate QR-code Data URI](#generate-qr-code-data-uri) - * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) - * [Callback URL validation](#validating-callback-url) - * [Querying sessions status](#session-status-request-handling-for-v31) - * [Sessions status response](#session-status-response) - * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) - * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) - * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) - * [Validating sessions status response](#validating-session-status-response) - * [Setting up CertificateValidator](#set-up-certificatevalidator) - * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) - * [Example of validating device link-based authentication session status](#device-link-based-authentication-session-status-validation) - * [Example of validating notification-based authentication session status](#notification-based-authentication-session-status-validation) - * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) - * [Example of validating the signature](#example-of-validating-the-signature-session-response) - * [Error handling for session status](#error-handling-for-session-status) - * [Certificate by document number](#certificate-by-document-number) - * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) - * [Linked signature session flow](#linked-signature-flow) - * [Device link certificate choice session](#device-link-certificate-choice-session) - * [Examples of initiating a device-link certificate choice session](#example-of-initiating-a-device-link-certificate-choice-session) - * [Linked notification-based signature session](#linked-notification-based-signature-session) - * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) - * [Notification-based flows](#notification-based-flows) - * [Differences between notification-based, device link-based flows and linked flows](#differences-between-notification-based-device-link-based-and-linked-flows) - * [Notification-based authentication session](#notification-based-authentication-session) - * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) - * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) - * [Initiating notification authentication session with semantics identifier](#initiating-a-notification-based-authentication-session-with-semantics-identifier) - * [Notification-based certificate choice session](#notification-based-certificate-choice-session) - * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) - * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) - * [Notification-based signature session](#notification-based-signature-session) - * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) - * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) - * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) - * [Examples of allowed notification-based interactions order](#examples-of-notification-based-interactions-order) - * [Exception handling](#exception-handling) - * [Network connection configuration of the client](#network-connection-configuration-of-the-client) - * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) - -## Introduction - -The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. -This library supports Smart-ID API v3.1. - -## Features - -* user authentication -* obtain user's signing certificate -* creating digital signature - -## Requirements - * Java 17 or 21 - -## Getting the library - -### Maven -You can use the library as a Maven dependency from the [Maven Central](https://search.maven.org/search?q=a:smart-id-java-client). - -```xml - - ee.sk.smartid - smart-id-java-client - INSERT_VERSION_HERE - -``` - -### Gradle - -`implementation 'ee.sk.smartid:smart-id-java-client:INSERT_VERSION_HERE'` - -## Changelog - -Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) - -# How to use API v3.1 - -Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. -This version introduces new device link and notification-based flows for authentication, certificate choice and signing. - -NB! v2 API classes are removed. - -To use the v3.1 API, import the relevant classes from the ee.sk.smartid package. - -```java - -import ee.sk.smartid.SmartIdConnector; -``` - -## Test accounts for testing - -[Test accounts for testing](https://sk-eid.github.io/smart-id-documentation/test_accounts.html) - - -## Logging - -### Log request payloads - -To log requests going to Smart-ID API set ee.sk.smartid.rest.LoggingFilter to log at trace level. -For applications on Spring Boot this can be done by adding following line to application.yml: -``` -logging.level.ee.sk.smartid.rest.LoggingFilter: trace -``` - -## Setting up SmartIdClient for v3.1 - -[Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) -NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO - -### Setting up SSL connection to Smart-ID API - -Live SSL certificates of Smart-ID service provider (SK) can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_rp_api_smart_id_com_certificates -Demo SSL certificates can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_sid_demo_sk_ee_certificates - -Recommended way is to use truststore and provide it to the client. - -```java -// Read truststore containing Smart-ID service provider (SK) SSL certificates -InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); - -// Initialize SmartIdClient and set connection parameters. -var smartIdClient = new SmartIdClient(); -// set relying party details -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -// set Smart-ID API host URL -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -// set the trust store containing SK SSL certificates -client.setTrustStore(trustStore); -``` - -### Provide SSL certificates to the client - -Also it is possible to add trusted certificates one by one. - -```java -// Initialize SmartIdClient and set connection parameters. -var smartIdClient = new SmartIdClient(); -// set relying party details -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -// set Smart-ID API host URL -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -// add trusted SSL certificates -client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj..."); -``` - -## Device-link flows - -Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. -More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html - -### Device-link authentication session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required base URI used to form device link or QR code. - -#### Examples of initiating a device-link authentication session - -##### Initiating an anonymous authentication session - -Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to authenticate users without knowing their identity. -RP can learn the user's identity only after the user has authenticated themselves. - -```java -// For security reasons a new hash value must be created for each new authentication request -String rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Set up builder -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link authentication session with semantics identifier - -More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "30303039914"); // identifier (according to country and identity type reference) - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link authentication session with document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating OK authentication sessions status response - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - - -##### Initiating a device-link authentication session with document number for Web2App flow - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating OK authentication sessions status response - -// Generate callback URL to be used for same device flows(Web2App, App2App) -CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("your-app://callback"); - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Set initial callback URL in the session request - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -Jump to [Validate callback URL](#validating-callback-url) for more info about validating callback URL. - -### Device-link signature session - -#### Request Parameters - -The request parameters for the device-link signature session are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response Parameters - -The response from a successful device-link signature session creation contains the following parameters: - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. - -#### Examples of initiating a device-link signature session - -##### Initiating a device-link signature session with semantics identifier - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Create the Semantics Identifier -var semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// Initiate the device-link signature -DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) - .initSignatureSession(); - -// Process the signature response -String sessionID = signatureResponse.sessionID(); -String sessionToken = signatureResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.sessionSecret(); -Instant receivedAt = signatureResponse.receivedAt(); -String deviceLinkBase = signatureResponse.deviceLinkBase(); - -// Generate QR-code or device link to be displayed to the user -// Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link signature session with document number - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Specify the document number -String documentNumber = "PNOEE-40504040001-MOCK-Q"; - -// Build the device-link signature request -DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// Process the signature response -String sessionID = signatureResponse.sessionID(); -String sessionToken = signatureResponse.sessionToken(); - -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.sessionSecret(); -Instant receivedAt = signatureResponse.receivedAt(); -String deviceLinkBase = signatureResponse.deviceLinkBase(); - -// Generate QR-code or device link to be displayed to the user -// Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Error Handling -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException` and others. - -```java -try { - DeviceLinkSessionResponse response = builder.init*Session(); -} catch (UserAccountNotFoundException e) { - System.out.println("User account not found."); -} catch (RelyingPartyAccountConfigurationException e) { - System.out.println("Relying party account configuration issue."); -} catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interaction is not supported by the user's app."); -} catch (ServerMaintenanceException e) { - System.out.println("Server maintenance in progress, please try again later."); -} catch (SmartIdClientException e) { - System.out.println("An error occurred: " + e.getMessage()); -} -``` - -### Additional device-link session request properties - -#### Using request properties to request the IP address of the user's device - -For the IP to be returned the service provider (SK) must switch on this option. -More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing - -Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. - -```java -DeviceLinkSessionResponse authenticationSessionResponse = client - .createDeviceLinkAuthentication() - .withRpChallenge(rpChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - // setting property to request the IP-address of the user's device - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); -``` - -### Examples of device link interactions - -An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. -For device link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. -DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons -and a second screen to enter the PIN-code. - -Below are examples of interaction elements specifically for device link flows: - -Example 1: `confirmationMessage` with fallback to `displayTextAndPIN` -Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. -```java -builder.withInteractions(List.of( - DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), - DeviceLinkInteraction.displayTextAndPin("Up to 60 characters of text here..") -)) -``` - -Example 2: `confirmationMessage` Only (No Fallback) -Description: Insist on `confirmationMessage`; -NB! If interactions is not supported the process will fail if fallback is not provided. -```java -builder.withInteractions(List.of( - DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") -)); -``` - -### Generating QR-code or device link - -Documentation to device link and QR-code requirements -https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html - -To use the Smart-ID **demo environment**, you must specify `smart-id-demo` as `schemeName`. -See: https://sk-eid.github.io/smart-id-documentation/environments.html#_demo - -#### Generating device link - -Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. - -##### Device link parameters - -* `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. -* `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. -* `version`: Version of the device link. Only allowed value is `"1.0"`. -* `deviceLinkType`: Type of the device link. Possible values are `QR`, `Web2App`, `App2App`. -* `sessionType`: Type of the sessions the device link is for. Possible values are `auth`, `sign`, `cert`. -* `sessionToken`: Token from the session response. -* `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` -* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a device link -* `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. -* `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. -* `interactions`: Base64-encoded JSON string of an array of interaction objects, used to calculate the authCode. -* `initialCallbackUrl`: Optional. Initial callback URL used for the same device(Web2App or App2App) device link flows. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. - -```java -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; - -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds since session response -long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); -``` - -##### Overriding default values - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withSchemeName("smart-id-demo") // override default scheme name to use demo environment - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .withInitialCallbackUrl("https://your-app/callback") - .buildDeviceLink(sessionResponse.sessionSecret()); -``` - -#### Generating QR-code - -Creating a QR code uses the Zxing library to generate a QR code image with device link as content. -According to link size the QR-code of version 9 (53x53 modules) is used. -For the QR-code to be scannable by most devices the QR code module size should be ~10px. -It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px). -Generated QR code will have error correction level low. - -##### Generate QR-code Data URI - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withElapsedSeconds(elapsedSeconds) - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); - -// Generate QR code image from device link URI -String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); -// Return Data URI to frontend and display the QR-code -``` - -##### Generate QR-code with custom height, width, quiet area and image format - -Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. -QR code version 9 (53x53 modules) is automatically selected by content size - -Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. -The width and height of 1159px produce a QR code with a module size of 19px. - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withElapsedSeconds(elapsedSeconds) - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); - -// Create QR-code with height and width of 570px and quiet area of 2 modules. -BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); -// Return Data URI to frontend and display the QR-code -String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); -``` -### Validating callback URL - -When using same device flows (Web2App or App2App) the initialCallbackUrl will be used by the Smart-ID app to redirect the user back to the Relying Party application. -Received callback URL will contain additional query parameters that must be validated by the Relying Party. - -Example of received callback URL for authentication: -`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc&userChallengeVerifier=XtPfaGa8JnGtYrJjboooUf0KfY9sMEHrWFpSQrsUv9c` - -Example of received callback URL for signature or certificate choice -`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc` - -1. RP must verify that the user sessions has `callbackUrl.urlToken()` with same value as in query parameter `value`. -2. RP must verify that the `sessionSecretDigest` query parameter matches the calculated digest created from session secret received in device link session init response. - For this library provides `CallbackUrlUtil.validateSessionSecretDigest(digestFromCallbackUrl, sessionSecret)` -3. For authentication same device flow RP also must verify the `userChallengeVerifier` query parameter. This can be done when polling the session status has finished and session status response has to be - validated. `deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, brokeredRpName);` - Value to validate `userChallengeVerifier` is in the session status response `signature.userChallenge`. - -## Session status request handling for v3.1 - -The Smart-ID v3.1 API includes new session status request path for retrieving session results. -Session status request is to be used for device link-based and notification-based flows. - -### Session status response - -The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: - -* `state`: RUNNING or COMPLETE -* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) -* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. -* `result.details`: Contains additional info when user refused interaction -* `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) -* `signature`: Contains the following fields based on the signatureProtocol used: - * For `ACSP_V2`: value, serverRandom, userChallenge, flowType, signatureAlgorithm, signatureAlgorithmParameters, - * For `RAW_DIGEST_SIGNATURE`: value, flowType, signatureAlgorithm, signatureAlgorithmParameters -* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). -* `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionTypeUsed`: The interaction type used for the session. -* `deviceIpAddress`: IP address of the mobile device, if requested. - -### Examples of querying session status in v3.1 - -#### Example of using session status poller to query final sessions status - -The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. - -```java -*SessionResponse sessionResponse; -// Get the session status poller -SessionsStatusPoller poller = client.getSessionsStatusPoller(); - -// Get sessionID from current session response -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) -if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ - System.out.println("Session completed with result: "+sessionStatus.getResult().getEndResult()); -} -``` - -#### Example of querying sessions status only once -The following example shows how to use the SessionStatusPoller to only query the sessions status single time. -NB! If using this method for device link-based flows. Make sure the pollingSleepTimeout is not set or does not impact generating the QR-code for every second. - -```java -*SessionResponse sessionResponse; -// Get the session status poller -SessionStatusPoller poller = client.getSessionStatusPoller(); - -// Querying the sessions status -SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.sessionID()); -// Checking sessions state -if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - // Session is still running and querying can be continued - // Dynamic content can be generated and displayed to the user -} else if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ - // continue to validate the sessions status -} else { - throw UnprocessableSmartIdResponseException("Invalid session state was returned"); -} -``` - -### Validating session status response - -It's important to validate the session status response to ensure that the returned signature or authentication result is valid. -For validating authentication session status response, use the `AuthenticationResponseValidator`. -For validating signature session status response, use the `SignatureResponseValidator`. -NB! Integrators must validate signature value against expected signature value. - -#### Set up CertificateValidator - -CertificateValidator will check if the certificate is not expired and is trusted -by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore. -Will be used by AuthenticationResponseValidator and SignatureResponseValidator. - -```java -// Set up TrustedCACertStore -// Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - -// Option 2 - initialize certificate store with custom locations for trust anchor truststore and for intermediate CA certificates -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() - .withTrustAnchorTruststorePath("path/to/trustAnchorTruststore.jks") - .withTrustAnchorTruststorePassword("password") - .withIntermediateCAStorePath("path/to/intermediateCAStore.jks") - .withIntermediateCAStorePassword("password") - .build(); - - -// Option 3 - Provide trust anchors and intermediate CA certificates directly -Set trustAnchors; -List intermediateCACertificates; -TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() - .withTrustAnchors(trustAnchors) - .withIntermediateCACertificates(intermediateCACertificates) - .build(); - -// Set up CertificateValidator with the trusted CA store -CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); -``` - -#### Example of validating the authentication sessions response: - -##### Device link-based authentication session status validation - -DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -// Set up AuthenticationResponseValidator with the CertificateValidator -DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); - -// Create authentication request builder -DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; -// Initialize session -DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); -// Get request used for starting the authentication session and use it later to validate sessions status response -DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); - -// get sessions result -SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// validate sessions state is completed -if("COMPLETE".equals(sessionStatus.getState())){ - // validate the session status response with authentication session request and return authentication identity - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); -} -``` - -##### Notification-based authentication session status validation - -NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -// Set up AuthenticationResponseValidator with the CertificateValidator -NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); - -// Create authentication request builder -NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; -// Initialize session -NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); -// Get request used for starting the authentication session and use it later to validate sessions status response -NotificationAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); - -// get sessions result -SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// validate sessions state is completed -if("COMPLETE".equals(sessionStatus.getState())){ - // validate the session status response with authentication session request and return authentication identity - AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); -} -``` - -#### Example of validating the certificate choice session response: - -CertificateChoiceResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -try { - // Set up CertificateChoiceResponseValidator with the CertificateValidator - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(sessionStatus); - -} catch (UserRefusedException e) { - System.out.println("User refused the session."); -} catch (SessionTimeoutException e) { - System.out.println("Session timed out."); -} catch (DocumentUnusableException e) { - System.out.println("Document is unusable for the session."); -} catch (SmartIdClientException e) { - System.out.println("An unexpected error occurred: " + e.getMessage()); -} -``` - -#### Example of validating the signature session response: - -SignatureResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -try { - // Objects needed for validation - CertificateResponse certResponse; // queried by document number or use CertificateChoiceResponse - SignableData signableData; // data that was sent for signing - // Initialize the signature response validator with CertificateValidator - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - // Validate signature value. This step can be skipped if other means of validating the signature value can be used. - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), - signableData.calculateHash(), - certResponse.certificate(), - signatureResponse.getRsaSsaPssParameters()); - - // Process the response (e.g., save to database or pass to another system) - handleSignatureResponse(signatureResponse); - -} catch (UserRefusedException e) { - System.out.println("User refused the session."); -} catch (SessionTimeoutException e) { - System.out.println("Session timed out."); -} catch (DocumentUnusableException e) { - System.out.println("Document is unusable for the session."); -} catch (SmartIdClientException e) { - System.out.println("An unexpected error occurred: " + e.getMessage()); -} -``` - -### Error handling for session status - -The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: - -* `OK`: Session completed successfully. -* `USER_REFUSED`: User refused the session. -* `TIMEOUT`: User did not respond in time. -* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. -* `WRONG_VC`: User selected the wrong verification code. -* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. -* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. -* `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction - was canceled. - * `displayTextAndPIN` - User pressed Cancel on PIN screen during displayTextAndPIN flow. - * `confirmationMessage` - User cancelled on confirmationMessage screen. - * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. -* `PROTOCOL_FAILURE`: An error occurred in the signing protocol. -* `EXPECTED_LINKED_SESSION`: RP has configured signature session that should follow device-link certificate choice session incorrectly and the process - cannot be completed. -* `SERVER_ERROR` - Technical error occurred at the server side and the process was terminated. - -## Certificate by document number - -In API v3.1 new endpoint was introduced to simplify querying certificate for signing. -RP can directly query the user's signing certificate by document number — no session flow or user interaction required. -Can be used for device link and notification-based signature flows. -Only requirement is that the device link authentication is successfully completed before to get the document number. - -### Request Parameters -The request parameters for the certificate by document number request are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are `ADVANCED`, `QUALIFIED` or `QSCD`. Defaults to `QUALIFIED`. - -### Response Parameters -* `state`: Required. Indicates result. Possible values: - * `OK`: Certificate found and returned. - * `DOCUMENT_UNUSABLE`: user's Smart-ID account is not usable for signing -* `cert`: Required. Object containing the signing certificate. - * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) - * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` - -### Example of querying certificate by document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// Build the certificate by document number request and query the certificate -CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - -// Set up the certificate validator -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); -CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - -// Validate the certificate -certificateValidator.validateCertificate(certResponse.certificate()); -``` -Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). - -## Linked signature flow - -In API v3.1 a new flow was introduced to link signature session to a previously completed certificate choice session. -The flow starts off with device link certificate choice session and must be followed by a linked notification-based signature session. - -### Device link certificate choice session - -Anonymous device link certificate choice session can be initiated without knowing the user's document number. When the session is completed successfully, -the Smart-ID API will stay waiting for the RP to start the [linked notification-based signature session](#linked-notification-based-signature-session). - -#### Request Parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotent behaviour. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. - -#### Example of initiating a device-link certificate choice session - -```java -DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) - .initiateCertificateChoice(); - -String sessionId = certificateChoice.sessionID(); -// SessionID is used to query sessions status later - -String sessionToken = certificateChoice.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = certificateChoice.sessionSecret(); -String deviceLinkBase = certificateChoice.deviceLinkBase(); -Instant responseReceivedAt = certificateChoice.receivedAt(); -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Linked notification-based signature session - -Second part of the linked signature flow. Will be used to start the signature session after the device link certificate choice session is completed successfully. - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `linkedSessionID`: Required. Session ID of the previously completed certificate choice session. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response parameters - -* `sessionID`: Required. String that can be used to request the signature session status result. - -#### Example of initiating a linked notification-based signature session - -```java -// Prerequisite: device link certificate choice has been completed successfully. -DeviceLinkSessionResponse certificateChoiceSessionResponse; -CertificateChoiceResponse certificateChoiceResponse; - -// Start the linked notification signature session using the sessionID from the certificate choice session -LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) - .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) - .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// SessionID is used to query sessions status later -String sessionId = signatureSessionResponse.sessionID(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -## Notification-based flows - -### Differences between notification-based, device link-based and linked flows - -* `Notification-Based flow` - * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. - * Known users or devices: - * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using device link-based flows. - * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. -* `Device Link flow` - * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. - * Anonymous authentication: the user's details are not required beforehand. RP validates the user after the Smart-ID authentication is completed. - * Real-time updates: QR-code needs to be refreshed every second to ensure validity. -* `Linked flow` - * Combination of anonymous certificate choice and notification-based signing: Starts with a device link-based certificate choice session followed by a notification-based signing session. - * QR-code or device link will be used only for the certificate choice part of the flow. - * Supports only device link-based interactions in the signature part of the flow. - -### Notification-based authentication session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `rpChallenge`: Required. Random value with size in range of 32-64 bytes. Must be Base64 encoded. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values is 'rsassa-pss' - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. An array of interaction objects defining the interactions in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -* `vcType`: Required. Type of verification code to be used. Currently, the only allowed value is `numeric4`. - -#### Response parameters -* `sessionID`: Required. String used to request the operation result. - -#### Examples of initiating a notification-based authentication session - -##### Initiating a notification-based authentication session with document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Generate verification code and display it to the user for confirmation -String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - .initAuthenticationSession(); - -// SessionID is used to query sessions status later -String sessionId = authenticationSessionResponse.sessionID(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a notification-based authentication session with semantics identifier - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Generate verification code and display it to the user for confirmation -String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into "))) // Display text should be concise and specific. - .initAuthenticationSession(); - -// SessionID can be used to query sessions status later -String sessionId = authenticationSessionResponse.sessionID(); - -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Notification-based certificate choice session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. - -#### Examples of initiating a notification-based certificate choice session - -##### Initiating a notification-based certificate choice session using semantics identifier - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - -// Use requested certificate level to validate certificate choice session status OK response. -CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" -NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(requestedCertificateLevel) - .initCertificateChoice(); - -String sessionId = certificateChoiceSessionResponse.sessionID(); -// SessionID is used to query sessions status later -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Notification-based signature session - -#### Request Parameters -The request parameters for the notification-based signature session are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only `rsassa-pss` is currently supported. - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm used for digest. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded string of interactions to be used for a session. The interactions are defined in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. To be used for overriding idempotency. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response Parameters -* `sessionID`: Required. String used to request the operation result. -* `vc`: Required. Object describing the verification code details. - * `type`: Required. Type of the verification code. Currently, the only allowed type is `numeric4`. - * `value`: Required. Value of the verification code to be displayed to the user. - -#### Examples of initiating a notification-based signature session - -##### Initiating a notification-based signature session with semantics identifier - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Create the Semantics Identifier -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// Build the notification signature request -NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the ")) // Display text should be concise and specific. - ) - .initSignatureSession(); - -// Get the session ID and continue to querying session status -String sessionID = signatureSessionResponse.sessionID(); - -// Display verification code to the user -String verificationCode = signatureSessionResponse.vc().getValue(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a notification-based signature session with document number - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Specify the document number -String documentNumber = "PNOEE-40504040001-MOCK-Q"; - -// Initiate the session -NotificationSignatureSessionResponse signatureResponse = client.createNotificationSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// Get the session ID and continue to querying session status -String sessionID = signatureResponse.sessionID(); - -// Display verification code to the user -String verificationCode = signatureResponse.vc().getValue(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Error Handling - -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: -* `UserAccountNotFoundException` -* `RelyingPartyAccountConfigurationException` -* `SessionNotFoundException` -* `RequiredInteractionNotSupportedByAppException` -* `ServerMaintenanceException` -* `SmartIdClientException` - -#### Example of Error Handling -```java -try { - NotificationSignatureSessionResponse response = builder.initSignatureSession(); -} catch (UserAccountNotFoundException e) { - System.out.println("User account not found."); -} catch (RelyingPartyAccountConfigurationException e) { - System.out.println("Relying party account configuration issue."); -} catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interaction is not supported by the user's app."); -} catch (ServerMaintenanceException e) { - System.out.println("Server maintenance in progress, please try again later."); -} catch (SmartIdClientException e) { - System.out.println("An error occurred: " + e.getMessage()); -} -``` - -### Additional notification-based session request parameters - -#### Using nonce to override idempotent behaviour - -Idempotent behaviour means that if the session request with same values is made multiple times within a 15-second window, -the same response with identical values will be returned. If there is a need to override this behaviour, a nonce can be used. -Nonce value must be a random string with a minimum length of 1 and a maximum length of 30 characters. - -Notification-based signature request is used as an example. Nonce can also be used with other signing session request -(device-link signature and certificate choice; notification-based certificate choice) by using method `withNonce("randomValue")`. - -```java -NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(Collections.singletonList( - NotificationInteraction.confirmationMessage("Please sign the ") // Display text should be concise and specific. - )) - // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned - // set nonce to override idempotent behaviour - .withNonce("randomValue") - .initSignatureSession(); -``` - -#### Using request properties to request the IP address of the user's device - -For the IP to be returned the service provider (SK) must switch on this option. -More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing - -Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. - -```java -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - // setting property to request the IP-address of the user's device - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); -``` - -### Examples of notification-based interactions order - -An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. -Different interactions can support different amounts of data to display information to the user. - -Below are examples of `interactions`. - -Example 1: `confirmationMessageAndVerificationCodeChoice` with fallback to `confirmationMessage` and with fallback to `displayTextAndPIN` -Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; The second choice is `confirmationMessage`; The third choice is `displayTextAndPIN`. -```java -builder.withInteractions(List.of( - NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), - NotificationInteraction.confirmationMessage("Up to 200 characters of text here..."), - NotificationInteraction.displayTextAndPin("Up to 60 characters of text here...") -)); -``` - -Example 2: `confirmationMessageAndVerificationCodeChoice` only -Description: Use `confirmationMessageAndVerificationCodeChoice` interaction exclusively. -NB! Process will fail when interaction is not supported and there is no fallback -```java -builder.withInteractions(List.of( - NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") -)); -``` - -## Exception Handling -The Smart-ID Java client library provides specific exceptions for different error scenarios. Handle exceptions appropriately to provide a good user experience. - -Exception Categories -* Permanent Exceptions - These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input - * `SmartIdClientException` Thrown for general client-side errors, such as: - * Missing or invalid configuration (e.g., `trustSslContext` not set). - * `SmartIdRequestSetupException` Thrown when the request field validations fails, such as: - * Missing required fields (e.g., `relyingPartyUUID`, `relyingPartyName`, `signatureProtocol`). - * Invalid values for fields (e.g. `interactionType` containing duplicate types). -* Unprocessable Response Exceptions - These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. - * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. - * Missing required fields (e.g., `state`, `endResult`, `signatureAlgorithm`). - * Incorrectly encoded Base64 strings in signature or certificate. - * Unexpected or unsupported `signatureProtocol`. -* User Action Exceptions - These exceptions cover scenarios where user actions or inactions lead to session termination or errors. - * `UserRefusedException` Base exception for user refusal scenarios. - * `SessionTimeoutException`: User did not respond within the allowed timeframe. - * `UserSelectedWrongVerificationCodeException` Thrown when the user selects an incorrect verification code during the process. -* User Account Exceptions - These exceptions handle issues related to the user's Smart-ID account or session requirements. - * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. - * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. - * `UserAccountUnusableException` Thrown when the user's Smart-ID account is not currently usable for the requested operation. -* Validation and Parsing Exceptions - These exceptions arise during validation or parsing operations within the library. - * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. - * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. -* Server side exceptions - * `ProtocolFailureException` Thrown when the Smart-ID API received invalid data such (f.e wrong data in generate device link) - * `ExpectedLinkedSessionException` Thrown when the Relying Party did not configure linked signature session to follow anonymous device-link certificate choice session. - * `SmartIdServerException` Thrown when the Smart-ID terminates the process due to a server-side error. - -## Network connection configuration of the client - -Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: - -- Initiation request -- Session status request - -Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. - -```java -SmartIdClient client = new SmartIdClient(); -// ... -// sets the timeout for each session status poll -client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); -// sets the pause between each session status poll -client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); -``` - -As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. - -Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig(); -clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); -clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); - -client.setNetworkConnectionConfig(clientConfig); -``` -And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); -RequestConfig reqConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(30000) - .setConnectionRequestTimeout(5000) - .build(); -clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); - -client.setNetworkConnectionConfig(clientConfig); -``` - -Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. - -### Example of creating a client with configured ssl context on JBoss using JAXWS RS - - -```java -ResteasyClient resteasyClient = new ResteasyClientBuilder() - .sslContext(SmartIdClient.createSslContext(Arrays.asList( - "pem cert 1", "pem cert 2"))) - .build(); - -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -client.setConfiguredClient(resteasyClient); +[![Tests](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml/badge.svg)](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml) +[![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) +[![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) +[![License: MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) + +# Smart-ID Java client + +This library supports Smart-ID API v3.1. + +## Table of contents + +* [Smart-ID Java client](#smart-id-java-client) + * [Introduction](#introduction) + * [Features](#features) + * [Requirements](#requirements) + * [Getting the library](#getting-the-library) + * [Changelog](#changelog) +* [How to use it with RP API v3.1](#how-to-use-api-v31) + * [Test accounts for testing](#test-accounts-for-testing) + * [Logging](#logging) + * [Log request payloads](#log-request-payloads) + * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) + * [Device link flows](#device-link-flows) + * [Device link authentication session](#device-link-authentication-session) + * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) + * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) + * [Initiating a device link-based authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) + * [Initiating a device link-based authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) + * [Device-link signature session](#device-link-signature-session) + * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) + * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) + * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) + * [Examples of allowed device-link interaction](#examples-of-device-link-interactions) + * [Additional request properties](#additional-device-link-session-request-properties) + * [Generating QR-code or device link](#generating-qr-code-or-device-link) + * [Generating device link ](#generating-device-link) + * [Device link parameters](#device-link-parameters) + * [Overriding default values](#overriding-default-values) + * [Generating QR-code](#generating-qr-code) + * [Generate QR-code Data URI](#generate-qr-code-data-uri) + * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) + * [Callback URL validation](#validating-callback-url) + * [Querying sessions status](#session-status-request-handling-for-v31) + * [Sessions status response](#session-status-response) + * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) + * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) + * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) + * [Validating sessions status response](#validating-session-status-response) + * [Setting up CertificateValidator](#set-up-certificatevalidator) + * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) + * [Example of validating device link-based authentication session status](#device-link-based-authentication-session-status-validation) + * [Example of validating notification-based authentication session status](#notification-based-authentication-session-status-validation) + * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) + * [Example of validating the signature](#example-of-validating-the-signature-session-response) + * [Error handling for session status](#error-handling-for-session-status) + * [Certificate by document number](#certificate-by-document-number) + * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) + * [Linked signature session flow](#linked-signature-flow) + * [Device link certificate choice session](#device-link-certificate-choice-session) + * [Examples of initiating a device-link certificate choice session](#example-of-initiating-a-device-link-certificate-choice-session) + * [Linked notification-based signature session](#linked-notification-based-signature-session) + * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) + * [Notification-based flows](#notification-based-flows) + * [Differences between notification-based, device link-based flows and linked flows](#differences-between-notification-based-device-link-based-and-linked-flows) + * [Notification-based authentication session](#notification-based-authentication-session) + * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) + * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) + * [Initiating notification authentication session with semantics identifier](#initiating-a-notification-based-authentication-session-with-semantics-identifier) + * [Notification-based certificate choice session](#notification-based-certificate-choice-session) + * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) + * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) + * [Notification-based signature session](#notification-based-signature-session) + * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) + * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) + * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) + * [Examples of allowed notification-based interactions order](#examples-of-notification-based-interactions-order) + * [Exception handling](#exception-handling) + * [Network connection configuration of the client](#network-connection-configuration-of-the-client) + * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) + +## Introduction + +The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. +This library supports Smart-ID API v3.1. + +## Features + +* user authentication +* obtain user's signing certificate +* creating digital signature + +## Requirements + * Java 17 or 21 + +## Getting the library + +### Maven +You can use the library as a Maven dependency from the [Maven Central](https://search.maven.org/search?q=a:smart-id-java-client). + +```xml + + ee.sk.smartid + smart-id-java-client + INSERT_VERSION_HERE + +``` + +### Gradle + +`implementation 'ee.sk.smartid:smart-id-java-client:INSERT_VERSION_HERE'` + +## Changelog + +Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) + +# How to use API v3.1 + +Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. +This version introduces new device link and notification-based flows for authentication, certificate choice and signing. + +NB! v2 API classes are removed. + +To use the v3.1 API, import the relevant classes from the ee.sk.smartid package. + +```java + +import ee.sk.smartid.SmartIdConnector; +``` + +## Test accounts for testing + +[Test accounts for testing](https://sk-eid.github.io/smart-id-documentation/test_accounts.html) + + +## Logging + +### Log request payloads + +To log requests going to Smart-ID API set ee.sk.smartid.rest.LoggingFilter to log at trace level. +For applications on Spring Boot this can be done by adding following line to application.yml: +``` +logging.level.ee.sk.smartid.rest.LoggingFilter: trace +``` + +## Setting up SmartIdClient for v3.1 + +[Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) +NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO + +### Setting up SSL connection to Smart-ID API + +Live SSL certificates of Smart-ID service provider (SK) can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_rp_api_smart_id_com_certificates +Demo SSL certificates can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_sid_demo_sk_ee_certificates + +Recommended way is to use truststore and provide it to the client. + +```java +// Read truststore containing Smart-ID service provider (SK) SSL certificates +InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); + +// Initialize SmartIdClient and set connection parameters. +var smartIdClient = new SmartIdClient(); +// set relying party details +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// set the trust store containing SK SSL certificates +client.setTrustStore(trustStore); +``` + +### Provide SSL certificates to the client + +Also it is possible to add trusted certificates one by one. + +```java +// Initialize SmartIdClient and set connection parameters. +var smartIdClient = new SmartIdClient(); +// set relying party details +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// add trusted SSL certificates +client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj..."); +``` + +## Device-link flows + +Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. +More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html + +### Device-link authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. + * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required base URI used to form device link or QR code. + +#### Examples of initiating a device-link authentication session + +##### Initiating an anonymous authentication session + +Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to authenticate users without knowing their identity. +RP can learn the user's identity only after the user has authenticated themselves. + +```java +// For security reasons a new hash value must be created for each new authentication request +String rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Set up builder +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link authentication session with semantics identifier + +More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "30303039914"); // identifier (according to country and identity type reference) + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link authentication session with document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating OK authentication sessions status response + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + + +##### Initiating a device-link authentication session with document number for Web2App flow + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating OK authentication sessions status response + +// Generate callback URL to be used for same device flows(Web2App, App2App) +CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("your-app://callback"); + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Set initial callback URL in the session request + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. +Jump to [Validate callback URL](#validating-callback-url) for more info about validating callback URL. + +### Device-link signature session + +#### Request Parameters + +The request parameters for the device-link signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response Parameters + +The response from a successful device-link signature session creation contains the following parameters: + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. + +#### Examples of initiating a device-link signature session + +##### Initiating a device-link signature session with semantics identifier + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Create the Semantics Identifier +var semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// Initiate the device-link signature +DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); + +// Generate QR-code or device link to be displayed to the user +// Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link signature session with document number + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Specify the document number +String documentNumber = "PNOEE-40504040001-MOCK-Q"; + +// Build the device-link signature request +DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); + +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); + +// Generate QR-code or device link to be displayed to the user +// Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Error Handling +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException` and others. + +```java +try { + DeviceLinkSessionResponse response = builder.init*Session(); +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { + System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { + System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { + System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { + System.out.println("An error occurred: " + e.getMessage()); +} +``` + +### Additional device-link session request properties + +#### Using request properties to request the IP address of the user's device + +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing + +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. + +```java +DeviceLinkSessionResponse authenticationSessionResponse = client + .createDeviceLinkAuthentication() + .withRpChallenge(rpChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); +``` + +### Examples of device link interactions + +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. +For device link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. +DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons +and a second screen to enter the PIN-code. + +Below are examples of interaction elements specifically for device link flows: + +Example 1: `confirmationMessage` with fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. +```java +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), + DeviceLinkInteraction.displayTextAndPin("Up to 60 characters of text here..") +)) +``` + +Example 2: `confirmationMessage` Only (No Fallback) +Description: Insist on `confirmationMessage`; +NB! If interactions is not supported the process will fail if fallback is not provided. +```java +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") +)); +``` + +### Generating QR-code or device link + +Documentation to device link and QR-code requirements +https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html + +To use the Smart-ID **demo environment**, you must specify `smart-id-demo` as `schemeName`. +See: https://sk-eid.github.io/smart-id-documentation/environments.html#_demo + +#### Generating device link + +Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. + +##### Device link parameters + +* `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. +* `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. +* `version`: Version of the device link. Only allowed value is `"1.0"`. +* `deviceLinkType`: Type of the device link. Possible values are `QR`, `Web2App`, `App2App`. +* `sessionType`: Type of the sessions the device link is for. Possible values are `auth`, `sign`, `cert`. +* `sessionToken`: Token from the session response. +* `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` +* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a device link +* `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. +* `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. +* `interactions`: Base64-encoded JSON string of an array of interaction objects, used to calculate the authCode. +* `initialCallbackUrl`: Optional. Initial callback URL used for the same device(Web2App or App2App) device link flows. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. + +```java +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; + +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds since session response +long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); +``` + +##### Overriding default values + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withSchemeName("smart-id-demo") // override default scheme name to use demo environment + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .withInitialCallbackUrl("https://your-app/callback") + .buildDeviceLink(sessionResponse.sessionSecret()); +``` + +#### Generating QR-code + +Creating a QR code uses the Zxing library to generate a QR code image with device link as content. +According to link size the QR-code of version 9 (53x53 modules) is used. +For the QR-code to be scannable by most devices the QR code module size should be ~10px. +It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px). +Generated QR code will have error correction level low. + +##### Generate QR-code Data URI + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); + +// Generate QR code image from device link URI +String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); +// Return Data URI to frontend and display the QR-code +``` + +##### Generate QR-code with custom height, width, quiet area and image format + +Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. +QR code version 9 (53x53 modules) is automatically selected by content size + +Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. +The width and height of 1159px produce a QR code with a module size of 19px. + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); + +// Create QR-code with height and width of 570px and quiet area of 2 modules. +BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); +// Return Data URI to frontend and display the QR-code +String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); +``` +### Validating callback URL + +When using same device flows (Web2App or App2App) the initialCallbackUrl will be used by the Smart-ID app to redirect the user back to the Relying Party application. +Received callback URL will contain additional query parameters that must be validated by the Relying Party. + +Example of received callback URL for authentication: +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc&userChallengeVerifier=XtPfaGa8JnGtYrJjboooUf0KfY9sMEHrWFpSQrsUv9c` + +Example of received callback URL for signature or certificate choice +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc` + +1. RP must verify that the user sessions has `callbackUrl.urlToken()` with same value as in query parameter `value`. +2. RP must verify that the `sessionSecretDigest` query parameter matches the calculated digest created from session secret received in device link session init response. + For this library provides `CallbackUrlUtil.validateSessionSecretDigest(digestFromCallbackUrl, sessionSecret)` +3. For authentication same device flow RP also must verify the `userChallengeVerifier` query parameter. This can be done when polling the session status has finished and session status response has to be + validated. `deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, brokeredRpName);` + Value to validate `userChallengeVerifier` is in the session status response `signature.userChallenge`. + +## Session status request handling for v3.1 + +The Smart-ID v3.1 API includes new session status request path for retrieving session results. +Session status request is to be used for device link-based and notification-based flows. + +### Session status response + +The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: + +* `state`: RUNNING or COMPLETE +* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) +* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. +* `result.details`: Contains additional info when user refused interaction +* `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) +* `signature`: Contains the following fields based on the signatureProtocol used: + * For `ACSP_V2`: value, serverRandom, userChallenge, flowType, signatureAlgorithm, signatureAlgorithmParameters, + * For `RAW_DIGEST_SIGNATURE`: value, flowType, signatureAlgorithm, signatureAlgorithmParameters +* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). +* `ignoredProperties`: Any unsupported or ignored properties from the request. +* `interactionTypeUsed`: The interaction type used for the session. +* `deviceIpAddress`: IP address of the mobile device, if requested. + +### Examples of querying session status in v3.1 + +#### Example of using session status poller to query final sessions status + +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +*SessionResponse sessionResponse; +// Get the session status poller +SessionsStatusPoller poller = client.getSessionsStatusPoller(); + +// Get sessionID from current session response +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) +if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + System.out.println("Session completed with result: "+sessionStatus.getResult().getEndResult()); +} +``` + +#### Example of querying sessions status only once +The following example shows how to use the SessionStatusPoller to only query the sessions status single time. +NB! If using this method for device link-based flows. Make sure the pollingSleepTimeout is not set or does not impact generating the QR-code for every second. + +```java +*SessionResponse sessionResponse; +// Get the session status poller +SessionStatusPoller poller = client.getSessionStatusPoller(); + +// Querying the sessions status +SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.sessionID()); +// Checking sessions state +if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + // Session is still running and querying can be continued + // Dynamic content can be generated and displayed to the user +} else if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + // continue to validate the sessions status +} else { + throw UnprocessableSmartIdResponseException("Invalid session state was returned"); +} +``` + +### Validating session status response + +It's important to validate the session status response to ensure that the returned signature or authentication result is valid. +For validating authentication session status response, use the `AuthenticationResponseValidator`. +For validating signature session status response, use the `SignatureResponseValidator`. +NB! Integrators must validate signature value against expected signature value. + +#### Set up CertificateValidator + +CertificateValidator will check if the certificate is not expired and is trusted +by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore. +Will be used by AuthenticationResponseValidator and SignatureResponseValidator. + +```java +// Set up TrustedCACertStore +// Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + +// Option 2 - initialize certificate store with custom locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() + .withTrustAnchorTruststorePath("path/to/trustAnchorTruststore.jks") + .withTrustAnchorTruststorePassword("password") + .withIntermediateCAStorePath("path/to/intermediateCAStore.jks") + .withIntermediateCAStorePassword("password") + .build(); + + +// Option 3 - Provide trust anchors and intermediate CA certificates directly +Set trustAnchors; +List intermediateCACertificates; +TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() + .withTrustAnchors(trustAnchors) + .withIntermediateCACertificates(intermediateCACertificates) + .build(); + +// Set up CertificateValidator with the trusted CA store +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); +``` + +#### Example of validating the authentication sessions response: + +##### Device link-based authentication session status validation + +DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +// Set up AuthenticationResponseValidator with the CertificateValidator +DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); + +// get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` + +##### Notification-based authentication session status validation + +NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +// Set up AuthenticationResponseValidator with the CertificateValidator +NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +NotificationAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); + +// get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` + +#### Example of validating the certificate choice session response: + +CertificateChoiceResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +try { + // Set up CertificateChoiceResponseValidator with the CertificateValidator + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(sessionStatus); + +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); +} +``` + +#### Example of validating the signature session response: + +SignatureResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +try { + // Objects needed for validation + CertificateResponse certResponse; // queried by document number or use CertificateChoiceResponse + SignableData signableData; // data that was sent for signing + // Initialize the signature response validator with CertificateValidator + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + // Validate signature value. This step can be skipped if other means of validating the signature value can be used. + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certResponse.certificate(), + signatureResponse.getRsaSsaPssParameters()); + + // Process the response (e.g., save to database or pass to another system) + handleSignatureResponse(signatureResponse); + +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); +} +``` + +### Error handling for session status + +The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: + +* `OK`: Session completed successfully. +* `USER_REFUSED`: User refused the session. +* `TIMEOUT`: User did not respond in time. +* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. +* `WRONG_VC`: User selected the wrong verification code. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. +* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. +* `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction + was canceled. + * `displayTextAndPIN` - User pressed Cancel on PIN screen during displayTextAndPIN flow. + * `confirmationMessage` - User cancelled on confirmationMessage screen. + * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. +* `PROTOCOL_FAILURE`: An error occurred in the signing protocol. +* `EXPECTED_LINKED_SESSION`: RP has configured signature session that should follow device-link certificate choice session incorrectly and the process + cannot be completed. +* `SERVER_ERROR` - Technical error occurred at the server side and the process was terminated. + +## Certificate by document number + +In API v3.1 new endpoint was introduced to simplify querying certificate for signing. +RP can directly query the user's signing certificate by document number — no session flow or user interaction required. +Can be used for device link and notification-based signature flows. +Only requirement is that the device link authentication is successfully completed before to get the document number. + +### Request Parameters +The request parameters for the certificate by document number request are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are `ADVANCED`, `QUALIFIED` or `QSCD`. Defaults to `QUALIFIED`. + +### Response Parameters +* `state`: Required. Indicates result. Possible values: + * `OK`: Certificate found and returned. + * `DOCUMENT_UNUSABLE`: user's Smart-ID account is not usable for signing +* `cert`: Required. Object containing the signing certificate. + * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) + * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` + +### Example of querying certificate by document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// Build the certificate by document number request and query the certificate +CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + +// Set up the certificate validator +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + +// Validate the certificate +certificateValidator.validateCertificate(certResponse.certificate()); +``` +Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). + +## Linked signature flow + +In API v3.1 a new flow was introduced to link signature session to a previously completed certificate choice session. +The flow starts off with device link certificate choice session and must be followed by a linked notification-based signature session. + +### Device link certificate choice session + +Anonymous device link certificate choice session can be initiated without knowing the user's document number. When the session is completed successfully, +the Smart-ID API will stay waiting for the RP to start the [linked notification-based signature session](#linked-notification-based-signature-session). + +#### Request Parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotent behaviour. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. +* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. + +#### Example of initiating a device-link certificate choice session + +```java +DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .initiateCertificateChoice(); + +String sessionId = certificateChoice.sessionID(); +// SessionID is used to query sessions status later + +String sessionToken = certificateChoice.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = certificateChoice.sessionSecret(); +String deviceLinkBase = certificateChoice.deviceLinkBase(); +Instant responseReceivedAt = certificateChoice.receivedAt(); +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Linked notification-based signature session + +Second part of the linked signature flow. Will be used to start the signature session after the device link certificate choice session is completed successfully. + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `linkedSessionID`: Required. Session ID of the previously completed certificate choice session. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response parameters + +* `sessionID`: Required. String that can be used to request the signature session status result. + +#### Example of initiating a linked notification-based signature session + +```java +// Prerequisite: device link certificate choice has been completed successfully. +DeviceLinkSessionResponse certificateChoiceSessionResponse; +CertificateChoiceResponse certificateChoiceResponse; + +// Start the linked notification signature session using the sessionID from the certificate choice session +LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) + .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// SessionID is used to query sessions status later +String sessionId = signatureSessionResponse.sessionID(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +## Notification-based flows + +### Differences between notification-based, device link-based and linked flows + +* `Notification-Based flow` + * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. + * Known users or devices: + * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using device link-based flows. + * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. +* `Device Link flow` + * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. + * Anonymous authentication: the user's details are not required beforehand. RP validates the user after the Smart-ID authentication is completed. + * Real-time updates: QR-code needs to be refreshed every second to ensure validity. +* `Linked flow` + * Combination of anonymous certificate choice and notification-based signing: Starts with a device link-based certificate choice session followed by a notification-based signing session. + * QR-code or device link will be used only for the certificate choice part of the flow. + * Supports only device link-based interactions in the signature part of the flow. + +### Notification-based authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. + * `rpChallenge`: Required. Random value with size in range of 32-64 bytes. Must be Base64 encoded. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values is 'rsassa-pss' + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. An array of interaction objects defining the interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `vcType`: Required. Type of verification code to be used. Currently, the only allowed value is `numeric4`. + +#### Response parameters +* `sessionID`: Required. String used to request the operation result. + +#### Examples of initiating a notification-based authentication session + +##### Initiating a notification-based authentication session with document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + .initAuthenticationSession(); + +// SessionID is used to query sessions status later +String sessionId = authenticationSessionResponse.sessionID(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based authentication session with semantics identifier + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into "))) // Display text should be concise and specific. + .initAuthenticationSession(); + +// SessionID can be used to query sessions status later +String sessionId = authenticationSessionResponse.sessionID(); + +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Notification-based certificate choice session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. + +#### Examples of initiating a notification-based certificate choice session + +##### Initiating a notification-based certificate choice session using semantics identifier + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + +// Use requested certificate level to validate certificate choice session status OK response. +CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(requestedCertificateLevel) + .initCertificateChoice(); + +String sessionId = certificateChoiceSessionResponse.sessionID(); +// SessionID is used to query sessions status later +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Notification-based signature session + +#### Request Parameters +The request parameters for the notification-based signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only `rsassa-pss` is currently supported. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm used for digest. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded string of interactions to be used for a session. The interactions are defined in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. To be used for overriding idempotency. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response Parameters +* `sessionID`: Required. String used to request the operation result. +* `vc`: Required. Object describing the verification code details. + * `type`: Required. Type of the verification code. Currently, the only allowed type is `numeric4`. + * `value`: Required. Value of the verification code to be displayed to the user. + +#### Examples of initiating a notification-based signature session + +##### Initiating a notification-based signature session with semantics identifier + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Create the Semantics Identifier +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// Build the notification signature request +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the ")) // Display text should be concise and specific. + ) + .initSignatureSession(); + +// Get the session ID and continue to querying session status +String sessionID = signatureSessionResponse.sessionID(); + +// Display verification code to the user +String verificationCode = signatureSessionResponse.vc().getValue(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based signature session with document number + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Specify the document number +String documentNumber = "PNOEE-40504040001-MOCK-Q"; + +// Initiate the session +NotificationSignatureSessionResponse signatureResponse = client.createNotificationSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// Get the session ID and continue to querying session status +String sessionID = signatureResponse.sessionID(); + +// Display verification code to the user +String verificationCode = signatureResponse.vc().getValue(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Error Handling + +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: +* `UserAccountNotFoundException` +* `RelyingPartyAccountConfigurationException` +* `SessionNotFoundException` +* `RequiredInteractionNotSupportedByAppException` +* `ServerMaintenanceException` +* `SmartIdClientException` + +#### Example of Error Handling +```java +try { + NotificationSignatureSessionResponse response = builder.initSignatureSession(); +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { + System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { + System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { + System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { + System.out.println("An error occurred: " + e.getMessage()); +} +``` + +### Additional notification-based session request parameters + +#### Using nonce to override idempotent behaviour + +Idempotent behaviour means that if the session request with same values is made multiple times within a 15-second window, +the same response with identical values will be returned. If there is a need to override this behaviour, a nonce can be used. +Nonce value must be a random string with a minimum length of 1 and a maximum length of 30 characters. + +Notification-based signature request is used as an example. Nonce can also be used with other signing session request +(device-link signature and certificate choice; notification-based certificate choice) by using method `withNonce("randomValue")`. + +```java +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(Collections.singletonList( + NotificationInteraction.confirmationMessage("Please sign the ") // Display text should be concise and specific. + )) + // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned + // set nonce to override idempotent behaviour + .withNonce("randomValue") + .initSignatureSession(); +``` + +#### Using request properties to request the IP address of the user's device + +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing + +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. + +```java +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); +``` + +### Examples of notification-based interactions order + +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. +Different interactions can support different amounts of data to display information to the user. + +Below are examples of `interactions`. + +Example 1: `confirmationMessageAndVerificationCodeChoice` with fallback to `confirmationMessage` and with fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; The second choice is `confirmationMessage`; The third choice is `displayTextAndPIN`. +```java +builder.withInteractions(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), + NotificationInteraction.confirmationMessage("Up to 200 characters of text here..."), + NotificationInteraction.displayTextAndPin("Up to 60 characters of text here...") +)); +``` + +Example 2: `confirmationMessageAndVerificationCodeChoice` only +Description: Use `confirmationMessageAndVerificationCodeChoice` interaction exclusively. +NB! Process will fail when interaction is not supported and there is no fallback +```java +builder.withInteractions(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") +)); +``` + +## Exception Handling +The Smart-ID Java client library provides specific exceptions for different error scenarios. Handle exceptions appropriately to provide a good user experience. + +Exception Categories +* Permanent Exceptions + These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input + * `SmartIdClientException` Thrown for general client-side errors, such as: + * Missing or invalid configuration (e.g., `trustSslContext` not set). + * `SmartIdRequestSetupException` Thrown when the request field validations fails, such as: + * Missing required fields (e.g., `relyingPartyUUID`, `relyingPartyName`, `signatureProtocol`). + * Invalid values for fields (e.g. `interactionType` containing duplicate types). +* Unprocessable Response Exceptions + These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. + * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. + * Missing required fields (e.g., `state`, `endResult`, `signatureAlgorithm`). + * Incorrectly encoded Base64 strings in signature or certificate. + * Unexpected or unsupported `signatureProtocol`. +* User Action Exceptions + These exceptions cover scenarios where user actions or inactions lead to session termination or errors. + * `UserRefusedException` Base exception for user refusal scenarios. + * `SessionTimeoutException`: User did not respond within the allowed timeframe. + * `UserSelectedWrongVerificationCodeException` Thrown when the user selects an incorrect verification code during the process. +* User Account Exceptions + These exceptions handle issues related to the user's Smart-ID account or session requirements. + * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. + * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. + * `UserAccountUnusableException` Thrown when the user's Smart-ID account is not currently usable for the requested operation. +* Validation and Parsing Exceptions + These exceptions arise during validation or parsing operations within the library. + * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. + * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. +* Server side exceptions + * `ProtocolFailureException` Thrown when the Smart-ID API received invalid data such (f.e wrong data in generate device link) + * `ExpectedLinkedSessionException` Thrown when the Relying Party did not configure linked signature session to follow anonymous device-link certificate choice session. + * `SmartIdServerException` Thrown when the Smart-ID terminates the process due to a server-side error. + +## Network connection configuration of the client + +Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: + +- Initiation request +- Session status request + +Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. + +```java +SmartIdClient client = new SmartIdClient(); +// ... +// sets the timeout for each session status poll +client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); +// sets the pause between each session status poll +client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); +``` + +As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. + +Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig(); +clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); +clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); + +client.setNetworkConnectionConfig(clientConfig); +``` +And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); +RequestConfig reqConfig = RequestConfig.custom() + .setConnectTimeout(5000) + .setSocketTimeout(30000) + .setConnectionRequestTimeout(5000) + .build(); +clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); + +client.setNetworkConnectionConfig(clientConfig); +``` + +Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. + +### Example of creating a client with configured ssl context on JBoss using JAXWS RS + + +```java +ResteasyClient resteasyClient = new ResteasyClientBuilder() + .sslContext(SmartIdClient.createSslContext(Arrays.asList( + "pem cert 1", "pem cert 2"))) + .build(); + +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setConfiguredClient(resteasyClient); ``` \ No newline at end of file diff --git a/mvnw b/mvnw index 5643201c..7cbf37ab 100755 --- a/mvnw +++ b/mvnw @@ -1,316 +1,316 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`\\unset -f command; \\command -v java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 8a15b7f3..23b7079a 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,188 +1,188 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 7e13153b..eb61823b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,300 +1,300 @@ - - - 4.0.0 - - ee.sk.smartid - smart-id-java-client - jar - 3.0-SNAPSHOT - - Smart-ID Java client - Smart-ID Java client is a Java library that can be used for easy integration of the Smart-ID solution to information systems or e-services - https://github.com/SK-EID/smart-id-java-client - - - MIT License - - - - scm:git:git@github.com:SK-EID/smart-id-java-client.git - scm:git:git@github.com:SK-EID/smart-id-java-client.git - https://github.com/SK-EID/smart-id-java-client.git - - - - - Juhan Aasaru - Nortal - https://www.nortal.com - - - Andreas Valdma - Nortal - https://www.nortal.com - - - - - SK ID Solutions AS - https://www.skidsolutions.eu/en - - - - UTF-8 - 17 - 17 - 2.17.2 - 2.17.2 - 3.1.8 - 6.2.10.Final - - - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${jersey.version} - - - org.glassfish.jersey.inject - jersey-hk2 - ${jersey.version} - - - - org.glassfish.jersey.connectors - jersey-apache-connector - ${jersey.version} - test - - - - org.glassfish.jaxb - jaxb-runtime - 4.0.5 - - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.annotations.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - - org.slf4j - slf4j-api - 2.0.16 - - - - jakarta.ws.rs - jakarta.ws.rs-api - 4.0.0 - - - - org.bouncycastle - bcprov-jdk18on - 1.78.1 - - - - com.google.zxing - core - 3.5.3 - - - com.google.zxing - javase - 3.5.3 - - - - org.junit.jupiter - junit-jupiter-api - 5.11.0 - test - - - org.junit.jupiter - junit-jupiter-params - 5.11.0 - test - - - org.hamcrest - hamcrest-library - 3.0 - test - - - ch.qos.logback - logback-classic - 1.5.8 - test - - - org.wiremock - wiremock - 3.9.1 - test - - - org.mockito - mockito-core - 5.13.0 - test - - - - org.jboss.resteasy - resteasy-client - ${resteasy.version} - test - - - - org.jboss.resteasy - resteasy-jackson2-provider - ${resteasy.version} - test - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.0 - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - - - prepare-agent - - - - report - test - - report - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.10.0 - - - attach-javadocs - - jar - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.4.0 - - - create-license-list - generate-resources - - aggregate-add-third-party - - - - - - mit - SK ID Solutions AS - Smart ID sample Java client - 2018 - UTF-8 - - src/main/java - src/test/java - - ${project.basedir}/src/license - ${project.basedir}/src/license/third-party-file-template.ftl - ${project.basedir} - LICENSE.3RD-PARTY - - - - - org.owasp - dependency-check-maven - 12.1.6 - - true - false - - - - - check - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.6.4 - - false - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - true - - - - - - - - - + + + 4.0.0 + + ee.sk.smartid + smart-id-java-client + jar + 3.0-SNAPSHOT + + Smart-ID Java client + Smart-ID Java client is a Java library that can be used for easy integration of the Smart-ID solution to information systems or e-services + https://github.com/SK-EID/smart-id-java-client + + + MIT License + + + + scm:git:git@github.com:SK-EID/smart-id-java-client.git + scm:git:git@github.com:SK-EID/smart-id-java-client.git + https://github.com/SK-EID/smart-id-java-client.git + + + + + Juhan Aasaru + Nortal + https://www.nortal.com + + + Andreas Valdma + Nortal + https://www.nortal.com + + + + + SK ID Solutions AS + https://www.skidsolutions.eu/en + + + + UTF-8 + 17 + 17 + 2.17.2 + 2.17.2 + 3.1.8 + 6.2.10.Final + + + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey.version} + test + + + + org.glassfish.jaxb + jaxb-runtime + 4.0.5 + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.annotations.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + org.slf4j + slf4j-api + 2.0.16 + + + + jakarta.ws.rs + jakarta.ws.rs-api + 4.0.0 + + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + + + + org.junit.jupiter + junit-jupiter-api + 5.11.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.11.0 + test + + + org.hamcrest + hamcrest-library + 3.0 + test + + + ch.qos.logback + logback-classic + 1.5.8 + test + + + org.wiremock + wiremock + 3.9.1 + test + + + org.mockito + mockito-core + 5.13.0 + test + + + + org.jboss.resteasy + resteasy-client + ${resteasy.version} + test + + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.10.0 + + + attach-javadocs + + jar + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.4.0 + + + create-license-list + generate-resources + + aggregate-add-third-party + + + + + + mit + SK ID Solutions AS + Smart ID sample Java client + 2018 + UTF-8 + + src/main/java + src/test/java + + ${project.basedir}/src/license + ${project.basedir}/src/license/third-party-file-template.ftl + ${project.basedir} + LICENSE.3RD-PARTY + + + + + org.owasp + dependency-check-maven + 12.1.6 + + true + false + + + + + check + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.4 + + false + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + true + + + + + + + + + diff --git a/src/license/LICENSE.EPL-1.0 b/src/license/LICENSE.EPL-1.0 index 8f48f685..027eadb1 100644 --- a/src/license/LICENSE.EPL-1.0 +++ b/src/license/LICENSE.EPL-1.0 @@ -1,204 +1,204 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC -LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM -CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - a) in the case of the initial Contributor, the initial code and - documentation distributed under this Agreement, and - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - -where such changes and/or additions to the Program originate from and are -distributed by that particular Contributor. A Contribution 'originates' from a -Contributor if it was added to the Program by such Contributor itself or -anyone acting on such Contributor's behalf. Contributions do not include -additions to the Program which: (i) are separate modules of software -distributed in conjunction with the Program under their own license agreement, -and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are -necessarily infringed by the use or sale of its Contribution alone or when -combined with the Program. - -"Program" means the Contributions distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, -including all Contributors. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free copyright license to - reproduce, prepare derivative works of, publicly display, publicly - perform, distribute and sublicense the Contribution of such Contributor, - if any, and such derivative works, in source code and object code form. - - b) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free patent license under - Licensed Patents to make, use, sell, offer to sell, import and otherwise - transfer the Contribution of such Contributor, if any, in source code and - object code form. This patent license shall apply to the combination of - the Contribution and the Program if, at the time the Contribution is - added by the Contributor, such addition of the Contribution causes such - combination to be covered by the Licensed Patents. The patent license - shall not apply to any other combinations which include the Contribution. - No hardware per se is licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the patent - or other intellectual property rights of any other entity. Each - Contributor disclaims any liability to Recipient for claims brought by - any other entity based on infringement of intellectual property rights or - otherwise. As a condition to exercising the rights and licenses granted - hereunder, each Recipient hereby assumes sole responsibility to secure - any other intellectual property rights needed, if any. For example, if a - third party patent license is required to allow Recipient to distribute - the Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has sufficient - copyright rights in its Contribution, if any, to grant the copyright - license set forth in this Agreement. - -3. REQUIREMENTS -A Contributor may choose to distribute the Program in object code form under -its own license agreement, provided that: - - a) it complies with the terms and conditions of this Agreement; and - - b) its license agreement: - i) effectively disclaims on behalf of all Contributors all - warranties and conditions, express and implied, including warranties - or conditions of title and non-infringement, and implied warranties - or conditions of merchantability and fitness for a particular - purpose; - ii) effectively excludes on behalf of all Contributors all liability - for damages, including direct, indirect, special, incidental and - consequential damages, such as lost profits; - iii) states that any provisions which differ from this Agreement are - offered by that Contributor alone and not by any other party; and - iv) states that source code for the Program is available from such - Contributor, and informs licensees how to obtain it in a reasonable - manner on or through a medium customarily used for software - exchange. - -When the Program is made available in source code form: - - a) it must be made available under this Agreement; and - - b) a copy of this Agreement must be included with each copy of the - Program. -Contributors may not remove or alter any copyright notices contained within -the Program. - -Each Contributor must identify itself as the originator of its Contribution, -if any, in a manner that reasonably allows subsequent Recipients to identify -the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION -Commercial distributors of software may accept certain responsibilities with -respect to end users, business partners and the like. While this license is -intended to facilitate the commercial use of the Program, the Contributor who -includes the Program in a commercial product offering should do so in a manner -which does not create potential liability for other Contributors. Therefore, -if a Contributor includes the Program in a commercial product offering, such -Contributor ("Commercial Contributor") hereby agrees to defend and indemnify -every other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits and -other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such Commercial -Contributor in connection with its distribution of the Program in a commercial -product offering. The obligations in this section do not apply to any claims -or Losses relating to any actual or alleged intellectual property -infringement. In order to qualify, an Indemnified Contributor must: a) -promptly notify the Commercial Contributor in writing of such claim, and b) -allow the Commercial Contributor to control, and cooperate with the Commercial -Contributor in, the defense and any related settlement negotiations. The -Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product -offering, Product X. That Contributor is then a Commercial Contributor. If -that Commercial Contributor then makes performance claims, or offers -warranties related to Product X, those performance claims and warranties are -such Commercial Contributor's responsibility alone. Under this section, the -Commercial Contributor would have to defend claims against the other -Contributors related to those performance claims and warranties, and if a -court requires any other Contributor to pay any damages as a result, the -Commercial Contributor must pay those damages. - -5. NO WARRANTY -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, -NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each -Recipient is solely responsible for determining the appropriateness of using -and distributing the Program and assumes all risks associated with its -exercise of rights under this Agreement , including but not limited to the -risks and costs of program errors, compliance with applicable laws, damage to -or loss of data, programs or equipment, and unavailability or interruption of -operations. - -6. DISCLAIMER OF LIABILITY -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY -CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION -LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY -OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of the -remainder of the terms of this Agreement, and without further action by the -parties hereto, such provision shall be reformed to the minimum extent -necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Program itself -(excluding combinations of the Program with other software or hardware) -infringes such Recipient's patent(s), then such Recipient's rights granted -under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to -comply with any of the material terms or conditions of this Agreement and does -not cure such failure in a reasonable period of time after becoming aware of -such noncompliance. If all Recipient's rights under this Agreement terminate, -Recipient agrees to cease use and distribution of the Program as soon as -reasonably practicable. However, Recipient's obligations under this Agreement -and any licenses granted by Recipient relating to the Program shall continue -and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in -order to avoid inconsistency the Agreement is copyrighted and may only be -modified in the following manner. The Agreement Steward reserves the right to -publish new versions (including revisions) of this Agreement from time to -time. No one other than the Agreement Steward has the right to modify this -Agreement. The Eclipse Foundation is the initial Agreement Steward. The -Eclipse Foundation may assign the responsibility to serve as the Agreement -Steward to a suitable separate entity. Each new version of the Agreement will -be given a distinguishing version number. The Program (including -Contributions) may always be distributed subject to the version of the -Agreement under which it was received. In addition, after a new version of the -Agreement is published, Contributor may elect to distribute the Program -(including its Contributions) under the new version. Except as expressly -stated in Sections 2(a) and 2(b) above, Recipient receives no rights or -licenses to the intellectual property of any Contributor under this Agreement, -whether expressly, by implication, estoppel or otherwise. All rights in the -Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the -intellectual property laws of the United States of America. No party to this -Agreement will bring a legal action under this Agreement more than one year -after the cause of action arose. Each party waives its rights to a jury trial -in any resulting litigation. +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or +anyone acting on such Contributor's behalf. Contributions do not include +additions to the Program which: (i) are separate modules of software +distributed in conjunction with the Program under their own license agreement, +and (ii) are not derivative works of the Program. +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such Contributor, + if any, and such derivative works, in source code and object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and otherwise + transfer the Contribution of such Contributor, if any, in source code and + object code form. This patent license shall apply to the combination of + the Contribution and the Program if, at the time the Contribution is + added by the Contributor, such addition of the Contribution causes such + combination to be covered by the Licensed Patents. The patent license + shall not apply to any other combinations which include the Contribution. + No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the patent + or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights or + otherwise. As a condition to exercising the rights and licenses granted + hereunder, each Recipient hereby assumes sole responsibility to secure + any other intellectual property rights needed, if any. For example, if a + third party patent license is required to allow Recipient to distribute + the Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + +3. REQUIREMENTS +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + i) effectively disclaims on behalf of all Contributors all + warranties and conditions, express and implied, including warranties + or conditions of title and non-infringement, and implied warranties + or conditions of merchantability and fitness for a particular + purpose; + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software + exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. +Contributors may not remove or alter any copyright notices contained within +the Program. + +Each Contributor must identify itself as the originator of its Contribution, +if any, in a manner that reasonably allows subsequent Recipients to identify +the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, +if a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits and +other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such Commercial +Contributor in connection with its distribution of the Program in a commercial +product offering. The obligations in this section do not apply to any claims +or Losses relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: a) +promptly notify the Commercial Contributor in writing of such claim, and b) +allow the Commercial Contributor to control, and cooperate with the Commercial +Contributor in, the defense and any related settlement negotiations. The +Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If +that Commercial Contributor then makes performance claims, or offers +warranties related to Product X, those performance claims and warranties are +such Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a +court requires any other Contributor to pay any damages as a result, the +Commercial Contributor must pay those damages. + +5. NO WARRANTY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using +and distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement , including but not limited to the +risks and costs of program errors, compliance with applicable laws, damage to +or loss of data, programs or equipment, and unavailability or interruption of +operations. + +6. DISCLAIMER OF LIABILITY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION +LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of the +remainder of the terms of this Agreement, and without further action by the +parties hereto, such provision shall be reformed to the minimum extent +necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program itself +(excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted +under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue +and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to +time. No one other than the Agreement Steward has the right to modify this +Agreement. The Eclipse Foundation is the initial Agreement Steward. The +Eclipse Foundation may assign the responsibility to serve as the Agreement +Steward to a suitable separate entity. Each new version of the Agreement will +be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the +Agreement under which it was received. In addition, after a new version of the +Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly +stated in Sections 2(a) and 2(b) above, Recipient receives no rights or +licenses to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in the +Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial +in any resulting litigation. diff --git a/src/license/third-party-file-template.ftl b/src/license/third-party-file-template.ftl index 5e58e2ce..f55b4289 100644 --- a/src/license/third-party-file-template.ftl +++ b/src/license/third-party-file-template.ftl @@ -1,21 +1,21 @@ -<#function licenseFormat licenses> - <#assign result = ""/> - <#list licenses as license> - <#assign result = result + " (" +license + ")"/> - - <#return result> - -<#function artifactFormat p> - <#if p.name?index_of('Unnamed') > -1> - <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> - <#else> - <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> - - -List of ${dependencyMap?size} third-party dependencies (auto-generated on ${.now?date?iso_utc} with License Maven Plugin): - -<#list dependencyMap as e> -<#assign project = e.getKey()/> -<#assign licenses = e.getValue()/> -* ${licenseFormat(licenses)} ${artifactFormat(project)} - +<#function licenseFormat licenses> + <#assign result = ""/> + <#list licenses as license> + <#assign result = result + " (" +license + ")"/> + + <#return result> + +<#function artifactFormat p> + <#if p.name?index_of('Unnamed') > -1> + <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> + <#else> + <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> + + +List of ${dependencyMap?size} third-party dependencies (auto-generated on ${.now?date?iso_utc} with License Maven Plugin): + +<#list dependencyMap as e> +<#assign project = e.getKey()/> +<#assign licenses = e.getValue()/> +* ${licenseFormat(licenses)} ${artifactFormat(project)} + diff --git a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java index f5f45506..31d1d721 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -1,71 +1,71 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents of authentication certificate levels. - */ -public enum AuthenticationCertificateLevel { - - /** - * Smart-ID basic certificate level. Use if you want to allow non-qualified and qualified accounts. - */ - ADVANCED(1), - /** - * Smart-ID highest certificate level. Use if you want to only allow qualified accounts. - */ - QUALIFIED(2); - - private final int level; - - AuthenticationCertificateLevel(int level) { - this.level = level; - } - - /** - * Check if current certificate level is same or higher than the given certificate level - * - * @param certificateLevel the level of the certificate - * @return the level of the certificate - */ - public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { - return this == certificateLevel || this.level > certificateLevel.level; - } - - /** - * Check if the given certificate level is supported - * - * @param certificateLevel the level of the certificate - * @return true if the level is supported, false otherwise - */ - public static boolean isSupported(String certificateLevel) { - return Arrays.stream(AuthenticationCertificateLevel.values()) - .anyMatch(cl -> cl.name().equals(certificateLevel)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents of authentication certificate levels. + */ +public enum AuthenticationCertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow non-qualified and qualified accounts. + */ + ADVANCED(1), + /** + * Smart-ID highest certificate level. Use if you want to only allow qualified accounts. + */ + QUALIFIED(2); + + private final int level; + + AuthenticationCertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return the level of the certificate + */ + public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { + return this == certificateLevel || this.level > certificateLevel.level; + } + + /** + * Check if the given certificate level is supported + * + * @param certificateLevel the level of the certificate + * @return true if the level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel) { + return Arrays.stream(AuthenticationCertificateLevel.values()) + .anyMatch(cl -> cl.name().equals(certificateLevel)); + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index e2559dc5..3dfaad43 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -1,185 +1,185 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -/** - * Represents users identity in the validated authentication certificate - */ -public class AuthenticationIdentity { - - private String givenName; - private String surname; - private String identityNumber; - private String country; - private X509Certificate authCertificate; - private LocalDate dateOfBirth; - - /** - * Initializes a new instance of the authentication identity. - */ - public AuthenticationIdentity() { - } - - /** - * Initializes a new instance of authentication identity with the authentication certificate. - * - * @param authCertificate the authentication certificate where the identity information is extracted from - */ - public AuthenticationIdentity(X509Certificate authCertificate) { - this.authCertificate = authCertificate; - } - - /** - * Gets the given name of the user. - * - * @return the given name of the user - */ - public String getGivenName() { - return givenName; - } - - /** - * Sets the given name of the user. - * - * @param givenName the given name of the user - */ - public void setGivenName(String givenName) { - this.givenName = givenName; - } - - /** - * Gets the surname of the user. - * - * @return the surname of the user - */ - public String getSurname() { - return surname; - } - - /** - * Sets the surname of the user. - * - * @param surname the surname of the user - */ - public void setSurname(String surname) { - this.surname = surname; - } - - /** - * Gets the identity number of the user. - * - * @return the identity number of the user - */ - public String getIdentityNumber() { - return identityNumber; - } - - /** - * Sets the identity number of the user. - *

- * The identity number is also known as national identification number, personal code, social security number etc. - *

- * Should be used if the value are only the numbers. F.e. 12345678901 - * - * @param identityNumber the identity number of the user - */ - public void setIdentityNumber(String identityNumber) { - this.identityNumber = identityNumber; - } - - /** - * Gets the identity number of the user. - * - * @return the identity code of the user - */ - public String getIdentityCode() { - return identityNumber; - } - - /** - * Sets the identity number of the user. - *

- * The identity number is also known as national identification number, personal code, social security number etc. - *

- * Should be used if the value contains alphanumeric characters. F.e. EE12345678901, 1234567-8901 - * - * @param identityCode the identity code of the user - */ - public void setIdentityCode(String identityCode) { - this.identityNumber = identityCode; - } - - /** - * Gets the country code of the user. - * - * @return the country code of the user - */ - public String getCountry() { - return country; - } - - /** - * Sets the country code of the user. - * - * @param country the country code of the user - */ - public void setCountry(String country) { - this.country = country; - } - - /** - * Gets the authentication certificate of the user. - * - * @return the authentication certificate of the user - */ - public X509Certificate getAuthCertificate() { - return authCertificate; - } - - /** - * Person's date of birth. - * - * @return Date of birth if this information is available in authentication response or empty optional. - */ - public Optional getDateOfBirth() { - return Optional.ofNullable(dateOfBirth); - } - - /** - * Sets person's date of birth. - * - * @param dateOfBirth Date of birth - */ - public void setDateOfBirth(LocalDate dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +/** + * Represents users identity in the validated authentication certificate + */ +public class AuthenticationIdentity { + + private String givenName; + private String surname; + private String identityNumber; + private String country; + private X509Certificate authCertificate; + private LocalDate dateOfBirth; + + /** + * Initializes a new instance of the authentication identity. + */ + public AuthenticationIdentity() { + } + + /** + * Initializes a new instance of authentication identity with the authentication certificate. + * + * @param authCertificate the authentication certificate where the identity information is extracted from + */ + public AuthenticationIdentity(X509Certificate authCertificate) { + this.authCertificate = authCertificate; + } + + /** + * Gets the given name of the user. + * + * @return the given name of the user + */ + public String getGivenName() { + return givenName; + } + + /** + * Sets the given name of the user. + * + * @param givenName the given name of the user + */ + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + /** + * Gets the surname of the user. + * + * @return the surname of the user + */ + public String getSurname() { + return surname; + } + + /** + * Sets the surname of the user. + * + * @param surname the surname of the user + */ + public void setSurname(String surname) { + this.surname = surname; + } + + /** + * Gets the identity number of the user. + * + * @return the identity number of the user + */ + public String getIdentityNumber() { + return identityNumber; + } + + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value are only the numbers. F.e. 12345678901 + * + * @param identityNumber the identity number of the user + */ + public void setIdentityNumber(String identityNumber) { + this.identityNumber = identityNumber; + } + + /** + * Gets the identity number of the user. + * + * @return the identity code of the user + */ + public String getIdentityCode() { + return identityNumber; + } + + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value contains alphanumeric characters. F.e. EE12345678901, 1234567-8901 + * + * @param identityCode the identity code of the user + */ + public void setIdentityCode(String identityCode) { + this.identityNumber = identityCode; + } + + /** + * Gets the country code of the user. + * + * @return the country code of the user + */ + public String getCountry() { + return country; + } + + /** + * Sets the country code of the user. + * + * @param country the country code of the user + */ + public void setCountry(String country) { + this.country = country; + } + + /** + * Gets the authentication certificate of the user. + * + * @return the authentication certificate of the user + */ + public X509Certificate getAuthCertificate() { + return authCertificate; + } + + /** + * Person's date of birth. + * + * @return Date of birth if this information is available in authentication response or empty optional. + */ + public Optional getDateOfBirth() { + return Optional.ofNullable(dateOfBirth); + } + + /** + * Sets person's date of birth. + * + * @param dateOfBirth Date of birth + */ + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } + +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java index bff9b6d6..abe3fb5f 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -import org.bouncycastle.asn1.x500.style.BCStyle; - -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.NationalIdentityNumberUtil; - -/** - * Maps X509 certificate to an {@link AuthenticationIdentity} object. - */ -public final class AuthenticationIdentityMapper { - - private AuthenticationIdentityMapper() { - } - - /** - * Maps the X509 certificate to an {@link AuthenticationIdentity} object. - * - * @param certificate Certificate to be converted to an {@link AuthenticationIdentity} object - * @return AuthenticationIdentity object - */ - public static AuthenticationIdentity from(X509Certificate certificate) { - var identity = new AuthenticationIdentity(certificate); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) - .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); - identity.setDateOfBirth(getDateOfBirth(identity)); - return identity; - } - - private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { - return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) - .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.bouncycastle.asn1.x500.style.BCStyle; + +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.NationalIdentityNumberUtil; + +/** + * Maps X509 certificate to an {@link AuthenticationIdentity} object. + */ +public final class AuthenticationIdentityMapper { + + private AuthenticationIdentityMapper() { + } + + /** + * Maps the X509 certificate to an {@link AuthenticationIdentity} object. + * + * @param certificate Certificate to be converted to an {@link AuthenticationIdentity} object + * @return AuthenticationIdentity object + */ + public static AuthenticationIdentity from(X509Certificate certificate) { + var identity = new AuthenticationIdentity(certificate); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) + .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); + identity.setDateOfBirth(getDateOfBirth(identity)); + return identity; + } + + private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { + return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) + .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index 25bdabba..7dc57cd9 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -1,267 +1,267 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.Base64; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * The authentication response after a successful authentication session status response was received. - *

- * Used with {@link DeviceLinkAuthenticationResponseValidator} to validate the certificate used for authentication - * and the signature in the authentication response. - */ -public class AuthenticationResponse { - - private String endResult; - private String serverRandom; - private String userChallenge; - private String signatureValueInBase64; - private X509Certificate certificate; - private AuthenticationCertificateLevel certificateLevel; - private String documentNumber; - private String interactionTypeUsed; - private FlowType flowType; - private String deviceIpAddress; - private RsaSsaPssParameters rsaSsaPssSignatureParameters; - - /** - * Gets the end result of the authentication session. - * - * @return the end result of the authentication session - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the authentication session. - * - * @param endResult the end result of the authentication session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the signature value in Base64 encoding. - * - * @return signature value in Base64 encoding - */ - public String getSignatureValueInBase64() { - return signatureValueInBase64; - } - - /** - * Sets the signature value in Base64 encoding. - * - * @param signatureValueInBase64 signature value in Base64 encoding - */ - public void setSignatureValueInBase64(String signatureValueInBase64) { - this.signatureValueInBase64 = signatureValueInBase64; - } - - /** - * Decodes Base64 encoded signature value and returns it as a byte array. - * - * @return signature value as a byte array - */ - public byte[] getSignatureValue() { - try { - return Base64.getDecoder().decode(signatureValueInBase64.getBytes(StandardCharsets.UTF_8)); - } catch (IllegalArgumentException e) { - throw new UnprocessableSmartIdResponseException( - "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); - } - } - - /** - * Get the certificate used in authentication. - * - * @return the X509Certificate used in authentication - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate used in authentication. - * - * @param certificate the X509Certificate used in authentication - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the level of the authentication certificate. - * - * @return the level of the authentication certificate - */ - public AuthenticationCertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the level of the authentication certificate. - * - * @param certificateLevel the authentication certificate level in the session status response - */ - public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the document number used for authentication - * - * @return the document number - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number used for authentication - * - * @param documentNumber the document number from the session status response - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - /** - * Gets the interaction type used in authentication - * - * @return the interaction type used in authentication - */ - public String getInteractionTypeUsed() { - return interactionTypeUsed; - } - - /** - * Sets the interaction type used in authentication - * - * @param interactionTypeUsed the interaction type used in authentication - */ - public void setInteractionTypeUsed(String interactionTypeUsed) { - this.interactionTypeUsed = interactionTypeUsed; - } - - /** - * Gets the IP address of the device used in authentication - * - * @return the IP address of the device - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in authentication - * - * @param deviceIpAddress the IP address of the device - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - - /** - * Gets the server random in Base64 encoding - * - * @return server random - */ - public String getServerRandom() { - return serverRandom; - } - - /** - * Sets the server random in Base64 encoding - * - * @param serverRandom the server random from the session status response - */ - public void setServerRandom(String serverRandom) { - this.serverRandom = serverRandom; - } - - /** - * Gets the user challenge - * - * @return user challenge - */ - public String getUserChallenge() { - return userChallenge; - } - - /** - * Sets the user challenge - * - * @param userChallenge the user challenge from the session status response - */ - public void setUserChallenge(String userChallenge) { - this.userChallenge = userChallenge; - } - - /** - * Gets the flow type user used to complete the authentication - *

- * - * @return flow type - */ - public FlowType getFlowType() { - return flowType; - } - - /** - * Sets the flow type used in authentication - * - * @param flowType the flow type used in authentication - */ - public void setFlowType(FlowType flowType) { - this.flowType = flowType; - } - - /** - * Gets the RSASSA-PSS parameters - * - * @return return RSASSA-PSS parameters - */ - public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { - return rsaSsaPssSignatureParameters; - } - - /** - * Sets the RSASSA-PSS parameters - * - * @param rsaSsaPssSignatureParameters the RSASSA-PSS parameters from the session status response - */ - public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { - this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * The authentication response after a successful authentication session status response was received. + *

+ * Used with {@link DeviceLinkAuthenticationResponseValidator} to validate the certificate used for authentication + * and the signature in the authentication response. + */ +public class AuthenticationResponse { + + private String endResult; + private String serverRandom; + private String userChallenge; + private String signatureValueInBase64; + private X509Certificate certificate; + private AuthenticationCertificateLevel certificateLevel; + private String documentNumber; + private String interactionTypeUsed; + private FlowType flowType; + private String deviceIpAddress; + private RsaSsaPssParameters rsaSsaPssSignatureParameters; + + /** + * Gets the end result of the authentication session. + * + * @return the end result of the authentication session + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the authentication session. + * + * @param endResult the end result of the authentication session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the signature value in Base64 encoding. + * + * @return signature value in Base64 encoding + */ + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + /** + * Sets the signature value in Base64 encoding. + * + * @param signatureValueInBase64 signature value in Base64 encoding + */ + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + /** + * Decodes Base64 encoded signature value and returns it as a byte array. + * + * @return signature value as a byte array + */ + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64.getBytes(StandardCharsets.UTF_8)); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + /** + * Get the certificate used in authentication. + * + * @return the X509Certificate used in authentication + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate used in authentication. + * + * @param certificate the X509Certificate used in authentication + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the level of the authentication certificate. + * + * @return the level of the authentication certificate + */ + public AuthenticationCertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the level of the authentication certificate. + * + * @param certificateLevel the authentication certificate level in the session status response + */ + public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the document number used for authentication + * + * @return the document number + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number used for authentication + * + * @param documentNumber the document number from the session status response + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + /** + * Gets the interaction type used in authentication + * + * @return the interaction type used in authentication + */ + public String getInteractionTypeUsed() { + return interactionTypeUsed; + } + + /** + * Sets the interaction type used in authentication + * + * @param interactionTypeUsed the interaction type used in authentication + */ + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; + } + + /** + * Gets the IP address of the device used in authentication + * + * @return the IP address of the device + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in authentication + * + * @param deviceIpAddress the IP address of the device + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + + /** + * Gets the server random in Base64 encoding + * + * @return server random + */ + public String getServerRandom() { + return serverRandom; + } + + /** + * Sets the server random in Base64 encoding + * + * @param serverRandom the server random from the session status response + */ + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } + + /** + * Gets the user challenge + * + * @return user challenge + */ + public String getUserChallenge() { + return userChallenge; + } + + /** + * Sets the user challenge + * + * @param userChallenge the user challenge from the session status response + */ + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + /** + * Gets the flow type user used to complete the authentication + *

+ * + * @return flow type + */ + public FlowType getFlowType() { + return flowType; + } + + /** + * Sets the flow type used in authentication + * + * @param flowType the flow type used in authentication + */ + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + + /** + * Gets the RSASSA-PSS parameters + * + * @return return RSASSA-PSS parameters + */ + public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { + return rsaSsaPssSignatureParameters; + } + + /** + * Sets the RSASSA-PSS parameters + * + * @param rsaSsaPssSignatureParameters the RSASSA-PSS parameters from the session status response + */ + public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { + this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index eb40e98c..9fa59666 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -1,47 +1,47 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * Represents a mapper for converting a SessionStatus to an AuthenticationResponse. - *

- * Used to map the received session status to an authentication response object. - *

- * Implementers should ensure that all mandatory fields are present. - */ -public interface AuthenticationResponseMapper { - - /** - * Validates the presence of mandatory fields and maps a SessionStatus to an AuthenticationResponse. - * - * @param sessionStatus the SessionStatus to map - * @return the mapped AuthenticationResponse - */ - AuthenticationResponse from(SessionStatus sessionStatus); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * Represents a mapper for converting a SessionStatus to an AuthenticationResponse. + *

+ * Used to map the received session status to an authentication response object. + *

+ * Implementers should ensure that all mandatory fields are present. + */ +public interface AuthenticationResponseMapper { + + /** + * Validates the presence of mandatory fields and maps a SessionStatus to an AuthenticationResponse. + * + * @param sessionStatus the SessionStatus to map + * @return the mapped AuthenticationResponse + */ + AuthenticationResponse from(SessionStatus sessionStatus); +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 75639014..156c19ac 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -1,278 +1,278 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Optional; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates and maps the received session status to authentication response - */ -public class AuthenticationResponseMapperImpl implements AuthenticationResponseMapper { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); - - private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; - private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; - private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; - - /** - * Maps session status to authentication response {@link AuthenticationResponse} - * - * @param sessionStatus session status received from Smart-ID server - * @return authentication response - */ - @Override - public AuthenticationResponse from(SessionStatus sessionStatus) { - validateSessionStatus(sessionStatus); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate sessionCertificate = sessionStatus.getCert(); - - var authenticationResponse = new AuthenticationResponse(); - authenticationResponse.setEndResult(sessionResult.getEndResult()); - authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); - authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); - authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - - var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - var rssSsaPssParameters = new RsaSsaPssParameters(); - rssSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); - rssSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm())); - rssSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); - rssSsaPssParameters.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - rssSsaPssParameters.setTrailerField(TrailerField.fromString(signatureAlgorithmParameters.getTrailerField())); - authenticationResponse.setRsaSsaPssSignatureParameters(rssSsaPssParameters); - - authenticationResponse.setCertificate(toCertificate(sessionCertificate)); - authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); - authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); - authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return authenticationResponse; - } - - private static void validateSessionStatus(SessionStatus sessionStatus) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionsStatus' is not provided"); - } - - validateResult(sessionStatus.getResult()); - validateSignatureProtocol(sessionStatus); - validateSignature(sessionStatus.getSignature()); - validateCertificate(sessionStatus.getCert()); - - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'interactionTypeUsed' is empty"); - } - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result' is empty"); - } - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.endResult' is empty"); - } - if (!"OK".equals(endResult)) { - ErrorResultHandler.handle(sessionResult); - } - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.documentNumber' is empty"); - } - } - - private static void validateSignatureProtocol(SessionStatus sessionStatus) { - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' is empty"); - } - - if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { - logger.error("Authentication session status field 'signatureProtocol' has invalid value: {}", sessionStatus.getSignatureProtocol()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' has unsupported value"); - } - } - - private static void validateSignature(SessionSignature sessionSignature) { - if (sessionSignature == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature' is missing"); - } - - if (StringUtil.isEmpty(sessionSignature.getValue())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' is empty"); - } - if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { - logger.error("Authentication session status field 'signature.value' does not have Base64-encoded value: {}", sessionSignature.getValue()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' does not have Base64-encoded value"); - } - - if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' is empty"); - } - int serverRandomLength = sessionSignature.getServerRandom().length(); - if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { - logger.error("Authentication session status field 'signature.serverRandom' is less than required length. Expected: {}; Actual: {}", MINIMUM_SERVER_RANDOM_LENGTH, serverRandomLength); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' value length is less than required"); - } - if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { - logger.error("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value: {}", sessionSignature.getServerRandom()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value"); - } - - if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' is empty"); - } - if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { - logger.error("Authentication session status field 'signature.userChallenge' does not match required pattern. Expected pattern {}; actual value {}", USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' value does not match required pattern"); - } - - if (StringUtil.isEmpty(sessionSignature.getFlowType())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' is empty"); - } - if (!FlowType.isSupported(sessionSignature.getFlowType())) { - logger.error("Authentication session status field 'signature.flowType' has invalid value: {}", sessionSignature.getFlowType()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' has unsupported value"); - } - - if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' is empty"); - } - if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { - logger.error("Authentication session status field 'signature.signatureAlgorithm' has invalid value: {}", sessionSignature.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' has unsupported value"); - } - - validateSignatureAlgorithmParameters(sessionSignature); - } - - private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { - var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - if (signatureAlgorithmParameters == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); - } - if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); - } - - Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); - if (hashAlgorithm.isEmpty()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", signatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); - } - - var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); - if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); - } - if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); - } - if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value"); - } - - if (maskGenAlgorithm.getParameters() == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); - } - if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); - } - Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); - if (maskGenHashAlgorithm.isEmpty()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); - } - if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", - hashAlgorithm.get().getAlgorithmName(), - maskGenHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); - } - - if (signatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty"); - } - int octetLength = hashAlgorithm.get().getOctetLength(); - if (octetLength != signatureAlgorithmParameters.getSaltLength()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: {}, actual: {}", - octetLength, - signatureAlgorithmParameters.getSaltLength()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); - } - - if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); - } - if (!TrailerField.BC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); - } - } - - private static void validateCertificate(SessionCertificate sessionCertificate) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert' is missing"); - } - - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.value' is empty"); - } - - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); - } - if (!AuthenticationCertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Authentication session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' has unsupported value"); - } - } - - private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { - return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - } - - private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { - return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to authentication response + */ +public class AuthenticationResponseMapperImpl implements AuthenticationResponseMapper { + + private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); + + private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; + private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; + private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; + + /** + * Maps session status to authentication response {@link AuthenticationResponse} + * + * @param sessionStatus session status received from Smart-ID server + * @return authentication response + */ + @Override + public AuthenticationResponse from(SessionStatus sessionStatus) { + validateSessionStatus(sessionStatus); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + + var authenticationResponse = new AuthenticationResponse(); + authenticationResponse.setEndResult(sessionResult.getEndResult()); + authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); + authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); + authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); + authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var rssSsaPssParameters = new RsaSsaPssParameters(); + rssSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm())); + rssSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rssSsaPssParameters.setTrailerField(TrailerField.fromString(signatureAlgorithmParameters.getTrailerField())); + authenticationResponse.setRsaSsaPssSignatureParameters(rssSsaPssParameters); + + authenticationResponse.setCertificate(toCertificate(sessionCertificate)); + authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); + authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); + authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return authenticationResponse; + } + + private static void validateSessionStatus(SessionStatus sessionStatus) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionsStatus' is not provided"); + } + + validateResult(sessionStatus.getResult()); + validateSignatureProtocol(sessionStatus); + validateSignature(sessionStatus.getSignature()); + validateCertificate(sessionStatus.getCert()); + + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'interactionTypeUsed' is empty"); + } + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result' is empty"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.endResult' is empty"); + } + if (!"OK".equals(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.documentNumber' is empty"); + } + } + + private static void validateSignatureProtocol(SessionStatus sessionStatus) { + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' is empty"); + } + + if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { + logger.error("Authentication session status field 'signatureProtocol' has invalid value: {}", sessionStatus.getSignatureProtocol()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' has unsupported value"); + } + } + + private static void validateSignature(SessionSignature sessionSignature) { + if (sessionSignature == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature' is missing"); + } + + if (StringUtil.isEmpty(sessionSignature.getValue())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' is empty"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { + logger.error("Authentication session status field 'signature.value' does not have Base64-encoded value: {}", sessionSignature.getValue()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' does not have Base64-encoded value"); + } + + if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' is empty"); + } + int serverRandomLength = sessionSignature.getServerRandom().length(); + if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { + logger.error("Authentication session status field 'signature.serverRandom' is less than required length. Expected: {}; Actual: {}", MINIMUM_SERVER_RANDOM_LENGTH, serverRandomLength); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' value length is less than required"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { + logger.error("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value: {}", sessionSignature.getServerRandom()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value"); + } + + if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' is empty"); + } + if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { + logger.error("Authentication session status field 'signature.userChallenge' does not match required pattern. Expected pattern {}; actual value {}", USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' value does not match required pattern"); + } + + if (StringUtil.isEmpty(sessionSignature.getFlowType())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' is empty"); + } + if (!FlowType.isSupported(sessionSignature.getFlowType())) { + logger.error("Authentication session status field 'signature.flowType' has invalid value: {}", sessionSignature.getFlowType()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' has unsupported value"); + } + + if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' is empty"); + } + if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { + logger.error("Authentication session status field 'signature.signatureAlgorithm' has invalid value: {}", sessionSignature.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' has unsupported value"); + } + + validateSignatureAlgorithmParameters(sessionSignature); + } + + private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + if (signatureAlgorithmParameters == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); + } + if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", signatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); + } + + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + } + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value"); + } + + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); + } + Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (maskGenHashAlgorithm.isEmpty()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); + } + if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", + hashAlgorithm.get().getAlgorithmName(), + maskGenHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + } + + if (signatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty"); + } + int octetLength = hashAlgorithm.get().getOctetLength(); + if (octetLength != signatureAlgorithmParameters.getSaltLength()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: {}, actual: {}", + octetLength, + signatureAlgorithmParameters.getSaltLength()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); + } + + if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); + } + if (!TrailerField.BC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); + } + } + + private static void validateCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert' is missing"); + } + + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.value' is empty"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); + } + if (!AuthenticationCertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Authentication session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' has unsupported value"); + } + } + + private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { + return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + } + + private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { + return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java index ba61db1c..22709c90 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -1,190 +1,190 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for constructing request to query certificate from Smart-ID API - */ -public class CertificateByDocumentNumberRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); - - private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); - - private final SmartIdConnector connector; - - private String documentNumber; - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel = CertificateLevel.QUALIFIED; - - /** - * Constructs a new CertificateByDocumentNumberRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public CertificateByDocumentNumberRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the document number for the request. - * - * @param documentNumber the document number - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the relying party UUID for the request. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name for the request. - * - * @param relyingPartyName the relying party name - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level for the request. - * - * @param certificateLevel the certificate level - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Builds the request and retrieves the certificate by document number. - * - * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate - * @throws SmartIdClientException if any required parameters are missing or invalid - * @throws UnprocessableSmartIdResponseException if the response is not valid - * @throws DocumentUnusableException if the document is unusable - */ - public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { - validateRequestParameters(); - var request = new CertificateByDocumentNumberRequest(relyingPartyUUID, relyingPartyName, certificateLevel == null ? null : certificateLevel.name()); - CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); - validateResponseParameters(response); - - return new CertificateByDocumentNumberResult( - CertificateLevel.valueOf(response.cert().certificateLevel()), - CertificateParser.parseX509Certificate(response.cert().value())); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(documentNumber)) { - throw new SmartIdClientException("Value for 'documentNumber' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdClientException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdClientException("Value for 'relyingPartyName' cannot be empty"); - } - } - - private void validateResponseParameters(CertificateResponse certificateResponse) { - if (certificateResponse == null) { - throw new UnprocessableSmartIdResponseException("Queried certificate response is not provided"); - } - validateState(certificateResponse); - - if (certificateResponse.cert() == null) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); - } - validateCertificateLevel(certificateResponse); - - if (StringUtil.isEmpty(certificateResponse.cert().value())) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' is missing"); - } - if (!BASE64_PATTERN.matcher(certificateResponse.cert().value()).matches()) { - logger.error("Certificate response field 'cert.value' has invalid value: {}", certificateResponse.cert().value()); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' does not have Base64-encoded value"); - } - } - - private static void validateState(CertificateResponse certificateResponse) { - String state = certificateResponse.state(); - if (StringUtil.isEmpty(state)) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); - } - if (!CertificateState.isSupported(state)) { - logger.error("Queried certificate response field 'state' has invalid value: {}", state); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' has unsupported value"); - } - if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { - throw new DocumentUnusableException(); - } - } - - private void validateCertificateLevel(CertificateResponse certificateResponse) { - String certificateLevel = certificateResponse.cert().certificateLevel(); - if (StringUtil.isEmpty(certificateLevel)) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); - } - if (!CertificateLevel.isSupported(certificateLevel)) { - logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); - } - CertificateLevel requestedLevel = this.certificateLevel == null ? CertificateLevel.QUALIFIED : this.certificateLevel; - if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(requestedLevel)) { - throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for constructing request to query certificate from Smart-ID API + */ +public class CertificateByDocumentNumberRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); + + private final SmartIdConnector connector; + + private String documentNumber; + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel = CertificateLevel.QUALIFIED; + + /** + * Constructs a new CertificateByDocumentNumberRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public CertificateByDocumentNumberRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the document number for the request. + * + * @param documentNumber the document number + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the relying party UUID for the request. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name for the request. + * + * @param relyingPartyName the relying party name + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level for the request. + * + * @param certificateLevel the certificate level + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Builds the request and retrieves the certificate by document number. + * + * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate + * @throws SmartIdClientException if any required parameters are missing or invalid + * @throws UnprocessableSmartIdResponseException if the response is not valid + * @throws DocumentUnusableException if the document is unusable + */ + public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { + validateRequestParameters(); + var request = new CertificateByDocumentNumberRequest(relyingPartyUUID, relyingPartyName, certificateLevel == null ? null : certificateLevel.name()); + CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); + validateResponseParameters(response); + + return new CertificateByDocumentNumberResult( + CertificateLevel.valueOf(response.cert().certificateLevel()), + CertificateParser.parseX509Certificate(response.cert().value())); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(documentNumber)) { + throw new SmartIdClientException("Value for 'documentNumber' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdClientException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdClientException("Value for 'relyingPartyName' cannot be empty"); + } + } + + private void validateResponseParameters(CertificateResponse certificateResponse) { + if (certificateResponse == null) { + throw new UnprocessableSmartIdResponseException("Queried certificate response is not provided"); + } + validateState(certificateResponse); + + if (certificateResponse.cert() == null) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); + } + validateCertificateLevel(certificateResponse); + + if (StringUtil.isEmpty(certificateResponse.cert().value())) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' is missing"); + } + if (!BASE64_PATTERN.matcher(certificateResponse.cert().value()).matches()) { + logger.error("Certificate response field 'cert.value' has invalid value: {}", certificateResponse.cert().value()); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' does not have Base64-encoded value"); + } + } + + private static void validateState(CertificateResponse certificateResponse) { + String state = certificateResponse.state(); + if (StringUtil.isEmpty(state)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); + } + if (!CertificateState.isSupported(state)) { + logger.error("Queried certificate response field 'state' has invalid value: {}", state); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' has unsupported value"); + } + if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { + throw new DocumentUnusableException(); + } + } + + private void validateCertificateLevel(CertificateResponse certificateResponse) { + String certificateLevel = certificateResponse.cert().certificateLevel(); + if (StringUtil.isEmpty(certificateLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); + } + if (!CertificateLevel.isSupported(certificateLevel)) { + logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); + } + CertificateLevel requestedLevel = this.certificateLevel == null ? CertificateLevel.QUALIFIED : this.certificateLevel; + if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(requestedLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java index cafdf002..5e942eec 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java @@ -1,38 +1,38 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -/** - * Result of querying certificate by document number. - * - * @param certificateLevel the level of the certificate - * @param certificate the X.509 certificate - */ -public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +/** + * Result of querying certificate by document number. + * + * @param certificateLevel the level of the certificate + * @param certificate the X.509 certificate + */ +public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { +} diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java index d9dce29a..15a1d1eb 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java @@ -1,140 +1,140 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -/** - * Represents the certificate choice response after a successful certificate choice sessions status response was received. - */ -public class CertificateChoiceResponse { - - private String endResult; - private X509Certificate certificate; - private CertificateLevel certificateLevel; - private String documentNumber; - private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name; Fix in SLIB-138 - private String deviceIpAddress; - - /** - * Gets the end result of the certificate choice session. - * - * @return the end result of the certificate choice session - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the certificate choice session. - * - * @param endResult the end result of the certificate choice session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the certificate chosen by the user during the certificate choice session. - * - * @return the certificate - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate chosen by the user during the certificate choice session. - * - * @param certificate the certificate from session status response - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the level of the certificate chosen by the user during the certificate choice session. - * - * @return the level of the certificate - */ - public CertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the level of the certificate chosen by the user during the certificate choice session. - * - * @param certificateLevel the level of the certificate from session status response - */ - public void setCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the document number of the user. - * - * @return the document number of the certificate - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number of the certificate chosen by the user during the certificate choice session. - * - * @param documentNumber the document number of the certificate from session status response - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * Gets the IP address of the device used in the certificate choice session. - * - * @return the IP address of the device - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in the certificate choice session. - * - * @param deviceIpAddress the IP address of the device from session status response - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +/** + * Represents the certificate choice response after a successful certificate choice sessions status response was received. + */ +public class CertificateChoiceResponse { + + private String endResult; + private X509Certificate certificate; + private CertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name; Fix in SLIB-138 + private String deviceIpAddress; + + /** + * Gets the end result of the certificate choice session. + * + * @return the end result of the certificate choice session + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the certificate choice session. + * + * @param endResult the end result of the certificate choice session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the certificate chosen by the user during the certificate choice session. + * + * @return the certificate + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate chosen by the user during the certificate choice session. + * + * @param certificate the certificate from session status response + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the level of the certificate chosen by the user during the certificate choice session. + * + * @return the level of the certificate + */ + public CertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the level of the certificate chosen by the user during the certificate choice session. + * + * @param certificateLevel the level of the certificate from session status response + */ + public void setCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the document number of the user. + * + * @return the document number of the certificate + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number of the certificate chosen by the user during the certificate choice session. + * + * @param documentNumber the document number of the certificate from session status response + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + /** + * Gets the IP address of the device used in the certificate choice session. + * + * @return the IP address of the device + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in the certificate choice session. + * + * @param deviceIpAddress the IP address of the device from session status response + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java index c4c76613..01c03b4e 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java @@ -1,168 +1,168 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates and maps the received session status to certificate choice response - */ -public class CertificateChoiceResponseValidator { - - private static final Logger logger = LoggerFactory.getLogger(CertificateChoiceResponseValidator.class); - - private final CertificateValidator certificateValidator; - private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; - - /** - * Initializes the certificate choice response validator with a certificate validator - * - * @param certificateValidator certificate validator to validate the received certificate - */ - public CertificateChoiceResponseValidator(CertificateValidator certificateValidator) { - this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Initializes the certificate choice response validator with a certificate validator and signature certificate purpose validator factory - * - * @param certificateValidator certificate validator to validate the received certificate - * @param signatureCertificatePurposeValidatorFactory factory to create signature certificate purpose validators - */ - public CertificateChoiceResponseValidator(CertificateValidator certificateValidator, - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; - } - - /** - * Validates certificate choice session status response - *

- * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level - * - * @param sessionStatus session status received from Smart-ID server - * @return certificate choice response {@link CertificateChoiceResponse} - */ - public CertificateChoiceResponse validate(SessionStatus sessionStatus) { - return validate(sessionStatus, CertificateLevel.QUALIFIED); - } - - /** - * Validates session status to certificate choice response with the requested certificate level - * - * @param sessionStatus session status received from Smart-ID server - * @param requestedCertificateLevel requested certificate level - * @return certificate choice response {@link CertificateChoiceResponse} - * @throws SmartIdClientException when the parameters are not provided - * @throws UnprocessableSmartIdResponseException when any required field is missing from the response or has invalid value - * @throws CertificateLevelMismatchException when the returned certificate level is lower than the requested one - */ - public CertificateChoiceResponse validate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (requestedCertificateLevel == null) { - throw new SmartIdClientException("Parameter 'requestedCertificateLevel' is not provided"); - } - validateResult(sessionStatus.getResult()); - SessionCertificate sessionCertificate = sessionStatus.getCert(); - validateSessionStatusCertificate(sessionCertificate); - CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - X509Certificate certificate = getValidateX509Certificate(sessionCertificate, certificateLevel, requestedCertificateLevel); - return toCertificateChoiceResponse(sessionStatus, certificate, certificateLevel); - } - - private X509Certificate getValidateX509Certificate(SessionCertificate sessionCertificate, - CertificateLevel certificateLevel, - CertificateLevel requestedCertificateLevel) { - if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException("Certificate choice session status response certificate level is lower than requested"); - } - X509Certificate certificate = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - certificateValidator.validate(certificate); - - SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); - purposeValidator.validate(certificate); - return certificate; - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result' is missing"); - } - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.endResult' is empty"); - } - if (!"OK".equalsIgnoreCase(endResult)) { - ErrorResultHandler.handle(sessionResult); - } - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.documentNumber' is empty"); - } - } - - private static void validateSessionStatusCertificate(SessionCertificate sessionCertificate) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert' is missing"); - } - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.value' has empty value"); - } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has empty value"); - } - if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Certificate choice session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has unsupported value"); - } - } - - private static CertificateChoiceResponse toCertificateChoiceResponse(SessionStatus sessionStatus, - X509Certificate certificate, - CertificateLevel certificateLevel) { - var certificateChoiceResponse = new CertificateChoiceResponse(); - certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); - certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); - certificateChoiceResponse.setCertificate(certificate); - certificateChoiceResponse.setCertificateLevel(certificateLevel); - certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return certificateChoiceResponse; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to certificate choice response + */ +public class CertificateChoiceResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateChoiceResponseValidator.class); + + private final CertificateValidator certificateValidator; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; + + /** + * Initializes the certificate choice response validator with a certificate validator + * + * @param certificateValidator certificate validator to validate the received certificate + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Initializes the certificate choice response validator with a certificate validator and signature certificate purpose validator factory + * + * @param certificateValidator certificate validator to validate the received certificate + * @param signatureCertificatePurposeValidatorFactory factory to create signature certificate purpose validators + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; + } + + /** + * Validates certificate choice session status response + *

+ * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @return certificate choice response {@link CertificateChoiceResponse} + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus) { + return validate(sessionStatus, CertificateLevel.QUALIFIED); + } + + /** + * Validates session status to certificate choice response with the requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @param requestedCertificateLevel requested certificate level + * @return certificate choice response {@link CertificateChoiceResponse} + * @throws SmartIdClientException when the parameters are not provided + * @throws UnprocessableSmartIdResponseException when any required field is missing from the response or has invalid value + * @throws CertificateLevelMismatchException when the returned certificate level is lower than the requested one + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (requestedCertificateLevel == null) { + throw new SmartIdClientException("Parameter 'requestedCertificateLevel' is not provided"); + } + validateResult(sessionStatus.getResult()); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + validateSessionStatusCertificate(sessionCertificate); + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + X509Certificate certificate = getValidateX509Certificate(sessionCertificate, certificateLevel, requestedCertificateLevel); + return toCertificateChoiceResponse(sessionStatus, certificate, certificateLevel); + } + + private X509Certificate getValidateX509Certificate(SessionCertificate sessionCertificate, + CertificateLevel certificateLevel, + CertificateLevel requestedCertificateLevel) { + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException("Certificate choice session status response certificate level is lower than requested"); + } + X509Certificate certificate = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + certificateValidator.validate(certificate); + + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); + return certificate; + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result' is missing"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.endResult' is empty"); + } + if (!"OK".equalsIgnoreCase(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.documentNumber' is empty"); + } + } + + private static void validateSessionStatusCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert' is missing"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.value' has empty value"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has empty value"); + } + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Certificate choice session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has unsupported value"); + } + } + + private static CertificateChoiceResponse toCertificateChoiceResponse(SessionStatus sessionStatus, + X509Certificate certificate, + CertificateLevel certificateLevel) { + var certificateChoiceResponse = new CertificateChoiceResponse(); + certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); + certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); + certificateChoiceResponse.setCertificate(certificate); + certificateChoiceResponse.setCertificateLevel(certificateLevel); + certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return certificateChoiceResponse; + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index d7373133..21d3bfd4 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -1,77 +1,77 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Representation of different signing certificate levels. - */ -public enum CertificateLevel { - - /** - * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. - */ - ADVANCED(1), - - /** - * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. - */ - QUALIFIED(2), - - /** - * Shortened alias for QUALIFIED level. - */ - QSCD(2); - - private final int level; - - CertificateLevel(int level) { - this.level = level; - } - - /** - * Check if current certificate level is same or higher than the given certificate level - * - * @param certificateLevel the level of the certificate - * @return true if the current level is same or higher than the given level, false otherwise - */ - public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { - return this == certificateLevel || this.level >= certificateLevel.level; - } - - /** - * Checks if the given certificate level value is supported - * - * @param certificateLevel the certificate level string to check - * @return true if the certificate level is supported, false otherwise - */ - public static boolean isSupported(String certificateLevel) { - return Arrays.stream(CertificateLevel.values()) - .anyMatch(level -> level.name().equals(certificateLevel)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Representation of different signing certificate levels. + */ +public enum CertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. + */ + ADVANCED(1), + + /** + * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. + */ + QUALIFIED(2), + + /** + * Shortened alias for QUALIFIED level. + */ + QSCD(2); + + private final int level; + + CertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return true if the current level is same or higher than the given level, false otherwise + */ + public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { + return this == certificateLevel || this.level >= certificateLevel.level; + } + + /** + * Checks if the given certificate level value is supported + * + * @param certificateLevel the certificate level string to check + * @return true if the certificate level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel) { + return Arrays.stream(CertificateLevel.values()) + .anyMatch(level -> level.name().equals(certificateLevel)); + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateParser.java b/src/main/java/ee/sk/smartid/CertificateParser.java index bbb00467..70f18ff9 100644 --- a/src/main/java/ee/sk/smartid/CertificateParser.java +++ b/src/main/java/ee/sk/smartid/CertificateParser.java @@ -1,73 +1,73 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for parsing X509 certificates from String values. - */ -public final class CertificateParser { - - private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); - - private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; - private static final String END_CERT = "-----END CERTIFICATE-----"; - - private CertificateParser() { - } - - /** - * Parses an X509 certificate from a String value. - * - * @param certificateValue the String value containing the certificate data - * @return the parsed X509Certificate - * @throws SmartIdClientException if the certificate cannot be parsed - */ - public static X509Certificate parseX509Certificate(String certificateValue) { - logger.debug("Parsing X509 certificate"); - String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( - StandardCharsets.UTF_8))); - } catch (CertificateException e) { - logger.error("Failed to parse X509 certificate from {}. Error {}", certificateString, e.getMessage()); - throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for parsing X509 certificates from String values. + */ +public final class CertificateParser { + + private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private CertificateParser() { + } + + /** + * Parses an X509 certificate from a String value. + * + * @param certificateValue the String value containing the certificate data + * @return the parsed X509Certificate + * @throws SmartIdClientException if the certificate cannot be parsed + */ + public static X509Certificate parseX509Certificate(String certificateValue) { + logger.debug("Parsing X509 certificate"); + String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( + StandardCharsets.UTF_8))); + } catch (CertificateException e) { + logger.error("Failed to parse X509 certificate from {}. Error {}", certificateString, e.getMessage()); + throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java index 6ade1924..405b4ae9 100644 --- a/src/main/java/ee/sk/smartid/CertificateState.java +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -1,57 +1,57 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Representation state of the queried certificate from the Smart-ID API. - */ -public enum CertificateState { - - /** - * Certificate is valid and can be used for signing. - */ - OK, - - /** - * There is an issue with the document. - */ - DOCUMENT_UNUSABLE; - - /** - * Checks if the given certificate state value is supported - * - * @param certificateState the certificate state string to check - * @return true if the certificate state is supported, false otherwise - */ - public static boolean isSupported(String certificateState) { - return Arrays.stream(CertificateState.values()) - .anyMatch(state -> state.name().equals(certificateState)); - } -} - +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Representation state of the queried certificate from the Smart-ID API. + */ +public enum CertificateState { + + /** + * Certificate is valid and can be used for signing. + */ + OK, + + /** + * There is an issue with the document. + */ + DOCUMENT_UNUSABLE; + + /** + * Checks if the given certificate state value is supported + * + * @param certificateState the certificate state string to check + * @return true if the certificate state is supported, false otherwise + */ + public static boolean isSupported(String certificateState) { + return Arrays.stream(CertificateState.values()) + .anyMatch(state -> state.name().equals(certificateState)); + } +} + diff --git a/src/main/java/ee/sk/smartid/CertificateValidator.java b/src/main/java/ee/sk/smartid/CertificateValidator.java index 0e499c12..1d74ddd0 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidator.java +++ b/src/main/java/ee/sk/smartid/CertificateValidator.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating X509 certificates used in Smart-ID authentication and signing. - *

- * Implementations of this interface should provide the logic to validate the certificate, - * ensuring it meets the necessary security and trust requirements. - */ -public interface CertificateValidator { - - /** - * Validates the given X509 certificate. - *

- * This method checks if the certificate is not expired and can be trusted - * - * @param certificate the X509Certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate is invalid - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating X509 certificates used in Smart-ID authentication and signing. + *

+ * Implementations of this interface should provide the logic to validate the certificate, + * ensuring it meets the necessary security and trust requirements. + */ +public interface CertificateValidator { + + /** + * Validates the given X509 certificate. + *

+ * This method checks if the certificate is not expired and can be trusted + * + * @param certificate the X509Certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is invalid + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java index 72dec7e1..1bac0ed8 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java @@ -1,106 +1,106 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertPathBuilder; -import java.security.cert.CertPathBuilderException; -import java.security.cert.CertStore; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.PKIXCertPathBuilderResult; -import java.security.cert.X509CertSelector; -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates the certificate's validity period and its trust chain. - */ -public class CertificateValidatorImpl implements CertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); - - private final TrustedCACertStore trustedCaCertStore; - - /** - * Constructs a certificate validator with the specified trusted certificate store. - * - * @param trustedCaCertStore the store containing trusted certificates. - */ - public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { - this.trustedCaCertStore = trustedCaCertStore; - } - - @Override - public void validate(X509Certificate certificate) { - validateCertificateIsCurrentlyValid(certificate); - validateCertificateChain(certificate); - } - - private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - logger.error("Certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); - throw new UnprocessableSmartIdResponseException("Certificate is invalid", ex); - } - } - - private void validateCertificateChain(X509Certificate certificate) { - try { - PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ - setCertificate(certificate); - }}); - CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); - params.addCertStore(intermediateStore); - params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); - CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); - PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); - - if (logger.isDebugEnabled()) { - X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); - X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); - X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", - CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); - } - } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { - throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates the certificate's validity period and its trust chain. + */ +public class CertificateValidatorImpl implements CertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); + + private final TrustedCACertStore trustedCaCertStore; + + /** + * Constructs a certificate validator with the specified trusted certificate store. + * + * @param trustedCaCertStore the store containing trusted certificates. + */ + public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { + this.trustedCaCertStore = trustedCaCertStore; + } + + @Override + public void validate(X509Certificate certificate) { + validateCertificateIsCurrentlyValid(certificate); + validateCertificateChain(certificate); + } + + private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Certificate is invalid", ex); + } + } + + private void validateCertificateChain(X509Certificate certificate) { + try { + PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ + setCertificate(certificate); + }}); + CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); + params.addCertStore(intermediateStore); + params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); + + if (logger.isDebugEnabled()) { + X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); + X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); + X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", + CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); + } + } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { + throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java index 10ad8eaa..f29469e1 100644 --- a/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java @@ -1,86 +1,86 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Implementation of the TrustedCAStore that manages a collection of trusted CA certificates. - */ -public class DefaultTrustedCACertStore implements TrustedCACertStore { - - private final Set trustAnchors = new HashSet<>(); - private final List trustedCACertificates = new ArrayList<>(); - private final boolean ocspEnabled; - - /** - * Initializes the trusted CA certificates from an array of X509 certificates. - * - * @param trustAnchors a set of TrustAnchor objects representing the trust anchors - * @param trustedCaCertificates a list of X509Certificate objects representing the trusted CA certificates - * @param ocspEnabled flag to disable or active OCSP validations - * - * @throws SmartIdClientException if the provided array is null or empty - */ - - public DefaultTrustedCACertStore(Set trustAnchors, List trustedCaCertificates, boolean ocspEnabled) { - this.trustAnchors.addAll(trustAnchors); - trustedCACertificates.addAll(trustedCaCertificates); - this.ocspEnabled = ocspEnabled; - } - - @Override - public List getTrustedCACertificates() { - return List.copyOf(trustedCACertificates); - } - - @Override - public Set getTrustAnchors() { - return Set.copyOf(trustAnchors); - } - - @Override - public boolean isOcspEnabled() { - return ocspEnabled; - } - - interface Builder { - /** - * Builds a new TrustedCAStoreImpl instance with the specified configuration. - * - * @return a new TrustedCAStoreImpl instance - */ - TrustedCACertStore build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Implementation of the TrustedCAStore that manages a collection of trusted CA certificates. + */ +public class DefaultTrustedCACertStore implements TrustedCACertStore { + + private final Set trustAnchors = new HashSet<>(); + private final List trustedCACertificates = new ArrayList<>(); + private final boolean ocspEnabled; + + /** + * Initializes the trusted CA certificates from an array of X509 certificates. + * + * @param trustAnchors a set of TrustAnchor objects representing the trust anchors + * @param trustedCaCertificates a list of X509Certificate objects representing the trusted CA certificates + * @param ocspEnabled flag to disable or active OCSP validations + * + * @throws SmartIdClientException if the provided array is null or empty + */ + + public DefaultTrustedCACertStore(Set trustAnchors, List trustedCaCertificates, boolean ocspEnabled) { + this.trustAnchors.addAll(trustAnchors); + trustedCACertificates.addAll(trustedCaCertificates); + this.ocspEnabled = ocspEnabled; + } + + @Override + public List getTrustedCACertificates() { + return List.copyOf(trustedCACertificates); + } + + @Override + public Set getTrustAnchors() { + return Set.copyOf(trustAnchors); + } + + @Override + public boolean isOcspEnabled() { + return ocspEnabled; + } + + interface Builder { + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instance + */ + TrustedCACertStore build(); + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java index 4244bd54..a00118d9 100644 --- a/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java @@ -1,158 +1,158 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.GeneralSecurityException; -import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertStore; -import java.security.cert.CertificateFactory; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXCertPathValidatorResult; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Builder for creating a DefaultTrustedCACertStore instance. - * This builder allows setting trust anchors, trusted CA certificates, and OCSP validation settings. - */ -public class DefaultTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { - - private static final Logger logger = LoggerFactory.getLogger(DefaultTrustedCAStoreBuilder.class); - - private Set trustAnchors; - private List intermediateCACertificates; - private boolean ocspEnabled = true; - private X509Certificate ocspValidationCert; - - /** - * Sets the trust anchors for the TrustedCAStore. - * - * @param trustAnchors a set of TrustAnchor objects to be used as trust anchors - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withTrustAnchors(Set trustAnchors) { - this.trustAnchors = trustAnchors; - return this; - } - - /** - * Sets the trusted CA certificates for the TrustedCAStore. - * - * @param intermediateCACertificates a list of X509Certificate objects to be used as trusted CA certificates - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withIntermediateCACertificate(List intermediateCACertificates) { - this.intermediateCACertificates = List.copyOf(intermediateCACertificates); - return this; - } - - /** - * Sets whether OCSP (Online Certificate Status Protocol) validation is enabled. - * - * @param enabled true to enable OCSP validation, false to disable it - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { - this.ocspEnabled = enabled; - return this; - } - - /** - * Sets the certificate used for OCSP validation. - * - * @param ocspValidationCert the X509Certificate to be used for OCSP validation - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withOCSPValidationCert(X509Certificate ocspValidationCert) { - this.ocspValidationCert = ocspValidationCert; - return this; - } - - @Override - public DefaultTrustedCACertStore build() { - if (!ocspEnabled) { - logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); - } else { - throw new UnsupportedOperationException("Does not work yet, will be implemented later"); - } - validateTrustAnchors(); - validateIntermediateCaCertificates(); - return new DefaultTrustedCACertStore(Set.copyOf(trustAnchors), List.copyOf(intermediateCACertificates), ocspEnabled); - } - - private void validateTrustAnchors() { - for (TrustAnchor trustAnchor : trustAnchors) { - try { - trustAnchor.getTrustedCert().verify(trustAnchor.getTrustedCert().getPublicKey()); - } catch (GeneralSecurityException e) { - throw new SmartIdClientException("", e); - } - } - } - - private void validateIntermediateCaCertificates() { - for (X509Certificate cert : intermediateCACertificates) { - validateIntermediateCACertificate(cert); - } - } - - private void validateIntermediateCACertificate(X509Certificate x509Certificates) { - try { - var cf = CertificateFactory.getInstance("X.509"); - CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); - var pkixParameters = new PKIXParameters(trustAnchors); - pkixParameters.setRevocationEnabled(ocspEnabled); - if (ocspEnabled) { - var certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(ocspValidationCert))); - pkixParameters.setCertStores(List.of(certStore)); - } - var certPathValidator = CertPathValidator.getInstance("PKIX"); - var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); - var trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); - } catch (GeneralSecurityException ex) { - logger.error("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); - throw new SmartIdClientException("Validating intermediate CA failed", ex); - } - } - - private String getCNValue(X509Certificate certificate) { - String subjectDN = certificate.getSubjectX500Principal().getName(); - return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertStore; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Builder for creating a DefaultTrustedCACertStore instance. + * This builder allows setting trust anchors, trusted CA certificates, and OCSP validation settings. + */ +public class DefaultTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(DefaultTrustedCAStoreBuilder.class); + + private Set trustAnchors; + private List intermediateCACertificates; + private boolean ocspEnabled = true; + private X509Certificate ocspValidationCert; + + /** + * Sets the trust anchors for the TrustedCAStore. + * + * @param trustAnchors a set of TrustAnchor objects to be used as trust anchors + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withTrustAnchors(Set trustAnchors) { + this.trustAnchors = trustAnchors; + return this; + } + + /** + * Sets the trusted CA certificates for the TrustedCAStore. + * + * @param intermediateCACertificates a list of X509Certificate objects to be used as trusted CA certificates + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withIntermediateCACertificate(List intermediateCACertificates) { + this.intermediateCACertificates = List.copyOf(intermediateCACertificates); + return this; + } + + /** + * Sets whether OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @param enabled true to enable OCSP validation, false to disable it + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Sets the certificate used for OCSP validation. + * + * @param ocspValidationCert the X509Certificate to be used for OCSP validation + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOCSPValidationCert(X509Certificate ocspValidationCert) { + this.ocspValidationCert = ocspValidationCert; + return this; + } + + @Override + public DefaultTrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("Does not work yet, will be implemented later"); + } + validateTrustAnchors(); + validateIntermediateCaCertificates(); + return new DefaultTrustedCACertStore(Set.copyOf(trustAnchors), List.copyOf(intermediateCACertificates), ocspEnabled); + } + + private void validateTrustAnchors() { + for (TrustAnchor trustAnchor : trustAnchors) { + try { + trustAnchor.getTrustedCert().verify(trustAnchor.getTrustedCert().getPublicKey()); + } catch (GeneralSecurityException e) { + throw new SmartIdClientException("", e); + } + } + } + + private void validateIntermediateCaCertificates() { + for (X509Certificate cert : intermediateCACertificates) { + validateIntermediateCACertificate(cert); + } + } + + private void validateIntermediateCACertificate(X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + if (ocspEnabled) { + var certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(ocspValidationCert))); + pkixParameters.setCertStores(List.of(certStore)); + } + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.error("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index 55496621..cf78d76b 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -1,216 +1,216 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates authentication response and converts it to {@link AuthenticationIdentity} - */ -public class DeviceLinkAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; - - /** - * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} - * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value - * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validator based on certificate level - */ - public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; - } - - /** - * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} using {@link CertificateValidator} - * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @return a new instance of {@link DeviceLinkAuthenticationResponseValidator} - */ - public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { - return new DeviceLinkAuthenticationResponseValidator(certificateValidator, - new AuthenticationResponseMapperImpl(), - new SignatureValueValidatorImpl(), - new AuthenticationCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates the authentication response contained in the session status using the provided authentication session request. - * - * @param sessionStatus the session status containing the authentication response to be validated - * @param authenticationSessionRequest the authentication session request used to initiate the authentication session - * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. - * Required only for same device flows. - * @param schemaName Schema name (RP name) used in the device link - * @return Authentication identity containing details about the authenticated user - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String userChallengeVerifier, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, null); - } - - /** - * Validates the authentication response contained in the session status using the provided authentication session request. - * - * @param sessionStatus the session status containing the authentication response to be validated - * @param authenticationSessionRequest the authentication session request used to initiate the authentication session - * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. - * Required only for same device flows. - * @param schemaName Schema name (RP name) used in the device link - * @param brokeredRpName the brokered RP name, used in the device link - * @return Authentication identity containing details about the authenticated user - * @throws UnprocessableSmartIdResponseException if the authentication response is invalid - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String userChallengeVerifier, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateUserChallenge(userChallengeVerifier, authenticationResponse); - validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - private void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { - return authenticationSessionRequest == null - ? AuthenticationCertificateLevel.QUALIFIED - : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - String[] payload = { - schemaName, - SignatureProtocol.ACSP_V2.name(), - authenticationResponse.getServerRandom(), - authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), - StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - toBase64(authenticationSessionRequest.relyingPartyName()), - StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), - authenticationResponse.getInteractionTypeUsed(), - authenticationResponse.getFlowType() == FlowType.QR ? "" : authenticationSessionRequest.initialCallbackUrl(), - authenticationResponse.getFlowType().getDescription() - }; - return String - .join("|", payload) - .getBytes(StandardCharsets.UTF_8); - } - - private static void validateUserChallenge(String userChallengeVerifier, AuthenticationResponse authenticationResponse) { - if (authenticationResponse.getFlowType() != FlowType.WEB2APP - && authenticationResponse.getFlowType() != FlowType.APP2APP) { - return; - } - if (StringUtil.isEmpty(userChallengeVerifier)) { - throw new SmartIdClientException("Parameter 'userChallengeVerifier' must be provided for 'flowType' - " + authenticationResponse.getFlowType()); - } - String userChallenge = authenticationResponse.getUserChallenge(); - String urlUserChallenge = toDigest(userChallengeVerifier); - if (!userChallenge.equals(urlUserChallenge)) { - throw new UnprocessableSmartIdResponseException("Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"); - } - } - - private static String toDigest(String userChallengeVerifier) { - byte[] userChallengeVerifierDigest = DigestCalculator.calculateDigest(userChallengeVerifier.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - return Base64.getUrlEncoder().withoutPadding().encodeToString(userChallengeVerifierDigest); - } - - private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private static String toBase64(String input) { - return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates authentication response and converts it to {@link AuthenticationIdentity} + */ +public class DeviceLinkAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + + /** + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validator based on certificate level + */ + public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + } + + /** + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @return a new instance of {@link DeviceLinkAuthenticationResponseValidator} + */ + public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new DeviceLinkAuthenticationResponseValidator(certificateValidator, + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @return Authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, null); + } + + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @param brokeredRpName the brokered RP name, used in the device link + * @return Authentication identity containing details about the authenticated user + * @throws UnprocessableSmartIdResponseException if the authentication response is invalid + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateUserChallenge(userChallengeVerifier, authenticationResponse); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), + authenticationResponse.getInteractionTypeUsed(), + authenticationResponse.getFlowType() == FlowType.QR ? "" : authenticationSessionRequest.initialCallbackUrl(), + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); + } + + private static void validateUserChallenge(String userChallengeVerifier, AuthenticationResponse authenticationResponse) { + if (authenticationResponse.getFlowType() != FlowType.WEB2APP + && authenticationResponse.getFlowType() != FlowType.APP2APP) { + return; + } + if (StringUtil.isEmpty(userChallengeVerifier)) { + throw new SmartIdClientException("Parameter 'userChallengeVerifier' must be provided for 'flowType' - " + authenticationResponse.getFlowType()); + } + String userChallenge = authenticationResponse.getUserChallenge(); + String urlUserChallenge = toDigest(userChallengeVerifier); + if (!userChallenge.equals(urlUserChallenge)) { + throw new UnprocessableSmartIdResponseException("Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"); + } + } + + private static String toDigest(String userChallengeVerifier) { + byte[] userChallengeVerifierDigest = DigestCalculator.calculateDigest(userChallengeVerifier.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(userChallengeVerifierDigest); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index a7451f7e..e46c6878 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -1,361 +1,361 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a device-link authentication session - */ -public class DeviceLinkAuthenticationSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; - private String rpChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; - private List interactions; - private Boolean shareMdClientIpAddress; - private Set capabilities; - private SemanticsIdentifier semanticsIdentifier; - private String documentNumber; - private String initialCallbackUrl; - - private DeviceLinkAuthenticationSessionRequest authenticationSessionRequest; - - /** - * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public DeviceLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartUUID the relying party UUID - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { - this.relyingPartyUUID = relyingPartUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - *

- * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the RP challenge. - *

- * RP challenge is a randomly generated string that must be Base64 encoded and - * should be regenerated for every new authentication session request. - *

- * You can use {@link ee.sk.smartid.RpChallengeGenerator} to generate a suitable RP challenge. - * - * @param rpChallenge RP challenge in Base64 encoded format - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { - this.rpChallenge = rpChallenge; - return this; - } - - /** - * Sets the signature algorithm - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the hash algorithm to be used for signature creation. - * By default, SHA3-512 is used. - * - * @param hashAlgorithm the hash algorithm to use - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return this; - } - - /** - * Sets the allowed interactions order - * - * @param interactions the allowed interactions order - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the authentication session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the document number - *

- * Setting this value will make the authentication session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the initial callback URL. - *

- * This URL is used to redirect the user after the authentication session is started. - *

- * The callback URL should be set when using same device flows (like Web2App or App2App). - * - * @param initialCallbackUrl the initial callback URL - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Sends the authentication request and get the init session response - *

- * There are 3 supported ways to start authentication session: - *

    - *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
  • with document number by using {@link #withDocumentNumber(String)}
  • - *
  • anonymously if semantics identifier and document number are not provided
  • - *
- * - * @return init session response - * @throws SmartIdRequestSetupException if the provided values for the request are invalid - * @throws UnprocessableSmartIdResponseException if the response is missing required fields - */ - public DeviceLinkSessionResponse initAuthenticationSession() { - validateRequestParameters(); - DeviceLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); - validateResponseParameters(deviceLinkAuthenticationSessionResponse); - this.authenticationSessionRequest = authenticationRequest; - return deviceLinkAuthenticationSessionResponse; - } - - /** - * Returns the authentication session request created during the initialization - * - * @return the authentication session request - * @throws SmartIdClientException when session is not yet initialized and method is called - */ - public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Device link authentication session has not been initialized yet"); - } - return authenticationSessionRequest; - } - - private DeviceLinkSessionResponse initAuthenticationSession(DeviceLinkAuthenticationSessionRequest authenticationRequest) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (semanticsIdentifier != null) { - return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initDeviceLinkAuthentication(authenticationRequest, documentNumber); - } else { - return connector.initAnonymousDeviceLinkAuthentication(authenticationRequest); - } - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - validateSignatureParameters(); - validateInteractions(); - validateInitialCallbackUrl(); - } - - private void validateSignatureParameters() { - if (StringUtil.isEmpty(rpChallenge)) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); - } - try { - Base64.getDecoder().decode(rpChallenge); - } catch (IllegalArgumentException e) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); - } - if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private DeviceLinkAuthenticationSessionRequest createAuthenticationRequest() { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); - - return new DeviceLinkAuthenticationSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2, - signatureProtocolParameters, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - capabilities, - initialCallbackUrl - ); - } - - private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); - } - if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null - || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a device-link authentication session + */ +public class DeviceLinkAuthenticationSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; + private String rpChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + private String initialCallbackUrl; + + private DeviceLinkAuthenticationSessionRequest authenticationSessionRequest; + + /** + * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DeviceLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + *

+ * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the RP challenge. + *

+ * RP challenge is a randomly generated string that must be Base64 encoded and + * should be regenerated for every new authentication session request. + *

+ * You can use {@link ee.sk.smartid.RpChallengeGenerator} to generate a suitable RP challenge. + * + * @param rpChallenge RP challenge in Base64 encoded format + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the hash algorithm to be used for signature creation. + * By default, SHA3-512 is used. + * + * @param hashAlgorithm the hash algorithm to use + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the allowed interactions order + * + * @param interactions the allowed interactions order + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

+ * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the initial callback URL. + *

+ * This URL is used to redirect the user after the authentication session is started. + *

+ * The callback URL should be set when using same device flows (like Web2App or App2App). + * + * @param initialCallbackUrl the initial callback URL + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

+ * There are 3 supported ways to start authentication session: + *

    + *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
  • with document number by using {@link #withDocumentNumber(String)}
  • + *
  • anonymously if semantics identifier and document number are not provided
  • + *
+ * + * @return init session response + * @throws SmartIdRequestSetupException if the provided values for the request are invalid + * @throws UnprocessableSmartIdResponseException if the response is missing required fields + */ + public DeviceLinkSessionResponse initAuthenticationSession() { + validateRequestParameters(); + DeviceLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(deviceLinkAuthenticationSessionResponse); + this.authenticationSessionRequest = authenticationRequest; + return deviceLinkAuthenticationSessionResponse; + } + + /** + * Returns the authentication session request created during the initialization + * + * @return the authentication session request + * @throws SmartIdClientException when session is not yet initialized and method is called + */ + public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Device link authentication session has not been initialized yet"); + } + return authenticationSessionRequest; + } + + private DeviceLinkSessionResponse initAuthenticationSession(DeviceLinkAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (semanticsIdentifier != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, documentNumber); + } else { + return connector.initAnonymousDeviceLinkAuthentication(authenticationRequest); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + validateSignatureParameters(); + validateInteractions(); + validateInitialCallbackUrl(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(rpChallenge)) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); + } + try { + Base64.getDecoder().decode(rpChallenge); + } catch (IllegalArgumentException e) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); + } + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private DeviceLinkAuthenticationSessionRequest createAuthenticationRequest() { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); + + return new DeviceLinkAuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + initialCallbackUrl + ); + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); + } + if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null + || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index a9a3f26b..915551c7 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -1,361 +1,361 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.StringUtil; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Builder for creating Smart-ID device-link URI. - */ -public class DeviceLinkBuilder { - - private static final String ALLOWED_VERSION = "1.0"; - - private String schemeName = "smart-id"; - private String deviceLinkBase; - private String version = ALLOWED_VERSION; - private DeviceLinkType deviceLinkType; - private SessionType sessionType; - private String sessionToken; - private Long elapsedSeconds; - private String lang; - - private String digest; - private String relyingPartyNameBase64; - private String brokeredRpNameBase64; - private String interactions; - private String initialCallbackUrl; - - /** - * Sets the scheme name for the device link. - *

- * Default is `smart-id`. - * - * @param schemeName the scheme name to be used in the device link - * @return this builder - */ - public DeviceLinkBuilder withSchemeName(String schemeName) { - this.schemeName = schemeName; - return this; - } - - /** - * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. - *

- * This is a required parameter and must be taken from the `deviceLinkBase` value received in the session-init response. - * - * @param deviceLinkBase the URL that will direct to SMART-ID application - * @return this builder - */ - public DeviceLinkBuilder withDeviceLinkBase(String deviceLinkBase) { - this.deviceLinkBase = deviceLinkBase; - return this; - } - - /** - * Sets the version of the device link. - *

- * Only value 1.0 is allowed - * - * @param version the version of - * @return this builder - */ - public DeviceLinkBuilder withVersion(String version) { - this.version = version; - return this; - } - - /** - * Sets the type of the device link. Use {@link DeviceLinkType} to set the type. - * - * @param deviceLinkType the type of the device link the builder is creating - * @return this builder - */ - public DeviceLinkBuilder withDeviceLinkType(DeviceLinkType deviceLinkType) { - this.deviceLinkType = deviceLinkType; - return this; - } - - /** - * Sets the type of the session. Use {@link SessionType} to set the type. - * - * @param sessionType the type of the session the device link is created for - * @return this builder - */ - public DeviceLinkBuilder withSessionType(SessionType sessionType) { - this.sessionType = sessionType; - return this; - } - - /** - * Sets the session token that was received from the Smart-ID server. - * - * @param sessionToken the session token that was received from the Smart-ID server - * @return this builder - */ - public DeviceLinkBuilder withSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - return this; - } - - /** - * Sets the time passed since the session response was received. - * Only valid for QR_CODE device link type. - * - * @param elapsedSeconds the time passed since the session response was received in seconds - * @return this builder - */ - public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { - this.elapsedSeconds = elapsedSeconds; - return this; - } - - /** - * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. - *

- * Default value is "eng". - * The value must match the language shown to the user in the UI. - * Also used for the fallback web page if the Smart-ID app is not installed. - * - * @param lang the language of the user - * @return this builder - */ - public DeviceLinkBuilder withLang(String lang) { - this.lang = lang; - return this; - } - - /** - * Sets the digest or rpChallenge used in the session. - * Required when signatureProtocol is defined. - * - * @param digest the digest or rpChallenge value - * @return this builder - */ - public DeviceLinkBuilder withDigest(String digest) { - this.digest = digest; - return this; - } - - /** - * Sets the relying party name which will be Base64-encoded using UTF-8. - * - * @param relyingPartyName relying party name as plain UTF-8 string - * @return this builder - */ - public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyNameBase64 = Base64.getEncoder().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); - return this; - } - - /** - * Sets the brokered relying party name which will be Base64-encoded using UTF-8. - * Leave empty if not acting as a broker. - * - * @param brokeredRpName brokered RP name as plain UTF-8 string - * @return this builder - */ - public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { - this.brokeredRpNameBase64 = Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); - return this; - } - - /** - * Sets the interactions used during session initiation as Base64 string. - * - * @param interactions interactions string in Base64 - * @return this builder - */ - public DeviceLinkBuilder withInteractions(String interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets the callback URL used in session initiation. - * Required only for same device flows (Web2App and App2App). - * Must be left empty for QR-code flow. - * - * @param initialCallbackUrl initial callback URL - * @return this builder - */ - public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Builds a Smart-ID device-link URI without authentication code. - *

- * The resulting URI is used in Web2App, App2App or QR-code flows, - * and must be combined with an authCode to form a valid device-link. - * - * @return unprotected device link URI - */ - public URI createUnprotectedUri() { - validateInputParameters(); - UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); - addElapsedSecondsIfQrCode(uriBuilder); - uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) - .queryParam("version", version).queryParam("lang", lang); - return uriBuilder.build(); - } - - /** - * Builds the final Smart-ID device link URI by combining unprotected link and authCode. - * - * @param sessionSecret session secret received from session initialization response. - * @return full device link URI with authCode parameter - */ - public URI buildDeviceLink(String sessionSecret) { - URI unprotectedUri = createUnprotectedUri(); - String authCode = generateAuthCode(unprotectedUri.toString(), sessionSecret); - return UriBuilder.fromUri(unprotectedUri) - .queryParam("authCode", authCode) - .build(); - } - - private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { - if (elapsedSeconds != null) { - if (deviceLinkType != DeviceLinkType.QR_CODE) { - throw new SmartIdClientException("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE"); - } - uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); - } - } - - private String generateAuthCode(String unprotectedLink, String sessionSecret) { - if (StringUtil.isEmpty(sessionSecret)) { - throw new SmartIdClientException("Parameter 'sessionSecret' cannot be empty"); - } - validateAuthCodeParams(); - return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); - } - - private String buildPayload(String unprotectedLink) { - return String.join("|", - schemeName, - getSignatureProtocolForSession(), - StringUtil.orEmpty(digest), - relyingPartyNameBase64, - StringUtil.orEmpty(brokeredRpNameBase64), - StringUtil.orEmpty(interactions), - StringUtil.orEmpty(initialCallbackUrl), - unprotectedLink - ); - } - - private String getSignatureProtocolForSession() { - return switch (sessionType) { - case AUTHENTICATION -> SignatureProtocol.ACSP_V2.name(); - case SIGNATURE -> SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); - case CERTIFICATE_CHOICE -> ""; - }; - } - - private String calculateAuthCode(String data, String base64Key) { - try { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); - byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); - } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException ex) { - throw new SmartIdClientException("Failed to calculate authCode", ex); - } - } - - private void validateInputParameters() { - if (StringUtil.isEmpty(deviceLinkBase)) { - throw new SmartIdClientException("Parameter 'deviceLinkBase' cannot be empty"); - } - if (StringUtil.isEmpty(version)) { - throw new SmartIdClientException("Parameter 'version' cannot be empty"); - } - if (!ALLOWED_VERSION.equals(version)) { - throw new SmartIdClientException("Only version 1.0 is allowed"); - } - if (deviceLinkType == null) { - throw new SmartIdClientException("Parameter 'deviceLinkType' must be set"); - } - if (sessionType == null) { - throw new SmartIdClientException("Parameter 'sessionType' must be set"); - } - if (StringUtil.isEmpty(sessionToken)) { - throw new SmartIdClientException("Parameter 'sessionToken' cannot be empty"); - } - if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { - throw new SmartIdClientException("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE"); - } - if (StringUtil.isEmpty(lang)) { - throw new SmartIdClientException("Parameter 'lang' must be set"); - } - } - - private void validateAuthCodeParams() { - if (StringUtil.isEmpty(schemeName)) { - throw new SmartIdClientException("Parameter 'schemeName' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyNameBase64)) { - throw new SmartIdClientException("Parameter 'relyingPartyName' cannot be empty"); - } - - boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); - if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { - throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE"); - } - if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { - throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP"); - } - if (sessionType != SessionType.CERTIFICATE_CHOICE) { - if (StringUtil.isEmpty(digest)) { - throw new SmartIdClientException("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); - } - if (StringUtil.isEmpty(interactions)) { - throw new SmartIdClientException("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); - } - } - if (sessionType == SessionType.CERTIFICATE_CHOICE) { - if (StringUtil.isNotEmpty(digest)) { - throw new SmartIdClientException("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); - } - if (StringUtil.isNotEmpty(interactions)) { - throw new SmartIdClientException("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); - } - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Builder for creating Smart-ID device-link URI. + */ +public class DeviceLinkBuilder { + + private static final String ALLOWED_VERSION = "1.0"; + + private String schemeName = "smart-id"; + private String deviceLinkBase; + private String version = ALLOWED_VERSION; + private DeviceLinkType deviceLinkType; + private SessionType sessionType; + private String sessionToken; + private Long elapsedSeconds; + private String lang; + + private String digest; + private String relyingPartyNameBase64; + private String brokeredRpNameBase64; + private String interactions; + private String initialCallbackUrl; + + /** + * Sets the scheme name for the device link. + *

+ * Default is `smart-id`. + * + * @param schemeName the scheme name to be used in the device link + * @return this builder + */ + public DeviceLinkBuilder withSchemeName(String schemeName) { + this.schemeName = schemeName; + return this; + } + + /** + * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. + *

+ * This is a required parameter and must be taken from the `deviceLinkBase` value received in the session-init response. + * + * @param deviceLinkBase the URL that will direct to SMART-ID application + * @return this builder + */ + public DeviceLinkBuilder withDeviceLinkBase(String deviceLinkBase) { + this.deviceLinkBase = deviceLinkBase; + return this; + } + + /** + * Sets the version of the device link. + *

+ * Only value 1.0 is allowed + * + * @param version the version of + * @return this builder + */ + public DeviceLinkBuilder withVersion(String version) { + this.version = version; + return this; + } + + /** + * Sets the type of the device link. Use {@link DeviceLinkType} to set the type. + * + * @param deviceLinkType the type of the device link the builder is creating + * @return this builder + */ + public DeviceLinkBuilder withDeviceLinkType(DeviceLinkType deviceLinkType) { + this.deviceLinkType = deviceLinkType; + return this; + } + + /** + * Sets the type of the session. Use {@link SessionType} to set the type. + * + * @param sessionType the type of the session the device link is created for + * @return this builder + */ + public DeviceLinkBuilder withSessionType(SessionType sessionType) { + this.sessionType = sessionType; + return this; + } + + /** + * Sets the session token that was received from the Smart-ID server. + * + * @param sessionToken the session token that was received from the Smart-ID server + * @return this builder + */ + public DeviceLinkBuilder withSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + /** + * Sets the time passed since the session response was received. + * Only valid for QR_CODE device link type. + * + * @param elapsedSeconds the time passed since the session response was received in seconds + * @return this builder + */ + public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { + this.elapsedSeconds = elapsedSeconds; + return this; + } + + /** + * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. + *

+ * Default value is "eng". + * The value must match the language shown to the user in the UI. + * Also used for the fallback web page if the Smart-ID app is not installed. + * + * @param lang the language of the user + * @return this builder + */ + public DeviceLinkBuilder withLang(String lang) { + this.lang = lang; + return this; + } + + /** + * Sets the digest or rpChallenge used in the session. + * Required when signatureProtocol is defined. + * + * @param digest the digest or rpChallenge value + * @return this builder + */ + public DeviceLinkBuilder withDigest(String digest) { + this.digest = digest; + return this; + } + + /** + * Sets the relying party name which will be Base64-encoded using UTF-8. + * + * @param relyingPartyName relying party name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyNameBase64 = Base64.getEncoder().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the brokered relying party name which will be Base64-encoded using UTF-8. + * Leave empty if not acting as a broker. + * + * @param brokeredRpName brokered RP name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { + this.brokeredRpNameBase64 = Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the interactions used during session initiation as Base64 string. + * + * @param interactions interactions string in Base64 + * @return this builder + */ + public DeviceLinkBuilder withInteractions(String interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets the callback URL used in session initiation. + * Required only for same device flows (Web2App and App2App). + * Must be left empty for QR-code flow. + * + * @param initialCallbackUrl initial callback URL + * @return this builder + */ + public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Builds a Smart-ID device-link URI without authentication code. + *

+ * The resulting URI is used in Web2App, App2App or QR-code flows, + * and must be combined with an authCode to form a valid device-link. + * + * @return unprotected device link URI + */ + public URI createUnprotectedUri() { + validateInputParameters(); + UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); + addElapsedSecondsIfQrCode(uriBuilder); + uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) + .queryParam("version", version).queryParam("lang", lang); + return uriBuilder.build(); + } + + /** + * Builds the final Smart-ID device link URI by combining unprotected link and authCode. + * + * @param sessionSecret session secret received from session initialization response. + * @return full device link URI with authCode parameter + */ + public URI buildDeviceLink(String sessionSecret) { + URI unprotectedUri = createUnprotectedUri(); + String authCode = generateAuthCode(unprotectedUri.toString(), sessionSecret); + return UriBuilder.fromUri(unprotectedUri) + .queryParam("authCode", authCode) + .build(); + } + + private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { + if (elapsedSeconds != null) { + if (deviceLinkType != DeviceLinkType.QR_CODE) { + throw new SmartIdClientException("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE"); + } + uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); + } + } + + private String generateAuthCode(String unprotectedLink, String sessionSecret) { + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter 'sessionSecret' cannot be empty"); + } + validateAuthCodeParams(); + return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); + } + + private String buildPayload(String unprotectedLink) { + return String.join("|", + schemeName, + getSignatureProtocolForSession(), + StringUtil.orEmpty(digest), + relyingPartyNameBase64, + StringUtil.orEmpty(brokeredRpNameBase64), + StringUtil.orEmpty(interactions), + StringUtil.orEmpty(initialCallbackUrl), + unprotectedLink + ); + } + + private String getSignatureProtocolForSession() { + return switch (sessionType) { + case AUTHENTICATION -> SignatureProtocol.ACSP_V2.name(); + case SIGNATURE -> SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); + case CERTIFICATE_CHOICE -> ""; + }; + } + + private String calculateAuthCode(String data, String base64Key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); + byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException ex) { + throw new SmartIdClientException("Failed to calculate authCode", ex); + } + } + + private void validateInputParameters() { + if (StringUtil.isEmpty(deviceLinkBase)) { + throw new SmartIdClientException("Parameter 'deviceLinkBase' cannot be empty"); + } + if (StringUtil.isEmpty(version)) { + throw new SmartIdClientException("Parameter 'version' cannot be empty"); + } + if (!ALLOWED_VERSION.equals(version)) { + throw new SmartIdClientException("Only version 1.0 is allowed"); + } + if (deviceLinkType == null) { + throw new SmartIdClientException("Parameter 'deviceLinkType' must be set"); + } + if (sessionType == null) { + throw new SmartIdClientException("Parameter 'sessionType' must be set"); + } + if (StringUtil.isEmpty(sessionToken)) { + throw new SmartIdClientException("Parameter 'sessionToken' cannot be empty"); + } + if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { + throw new SmartIdClientException("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE"); + } + if (StringUtil.isEmpty(lang)) { + throw new SmartIdClientException("Parameter 'lang' must be set"); + } + } + + private void validateAuthCodeParams() { + if (StringUtil.isEmpty(schemeName)) { + throw new SmartIdClientException("Parameter 'schemeName' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyNameBase64)) { + throw new SmartIdClientException("Parameter 'relyingPartyName' cannot be empty"); + } + + boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); + if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE"); + } + if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP"); + } + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } + if (StringUtil.isEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } + } + if (sessionType == SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isNotEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } + if (StringUtil.isNotEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index 423a189e..f1971dc2 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -1,212 +1,212 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.util.StringUtil.isEmpty; - -import java.util.Set; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder class for creating and initializing a device link-based certificate choice session. - */ -public class DeviceLinkCertificateChoiceSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private Boolean shareMdClientIpAddress; - private String initialCallbackUrl; - - /** - * Constructs a new DeviceLinkCertificateRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce - * - * @param nonce the nonce - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the initial callback URL for the device link session. - * This URL is used to redirect the user after the session is initialized. - * - * @param initialCallbackUrl the initial callback URL - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Starts a device link-based certificate choice session and returns the session response. - * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, - * which can be used by the Relying Party to manage and verify the session independently. - * - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. - * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. - * @throws UnprocessableSmartIdResponseException if the response is missing required fields. - */ - public DeviceLinkSessionResponse initCertificateChoice() { - validateRequestParameters(); - DeviceLinkCertificateChoiceSessionRequest deviceLinkCertificateChoiceSessionRequest = createCertificateRequest(); - DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(deviceLinkCertificateChoiceSessionRequest); - validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); - return deviceLinkCertificateChoiceSessionResponse; - } - - private void validateRequestParameters() { - if (isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' must have length between 1 and 30 characters"); - } - validateInitialCallbackUrl(); - } - - private DeviceLinkCertificateChoiceSessionRequest createCertificateRequest() { - return new DeviceLinkCertificateChoiceSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - nonce, - capabilities, - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - initialCallbackUrl - ); - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); - } - - if (deviceLinkCertificateChoiceSessionResponse.deviceLinkBase() == null - || deviceLinkCertificateChoiceSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.util.StringUtil.isEmpty; + +import java.util.Set; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder class for creating and initializing a device link-based certificate choice session. + */ +public class DeviceLinkCertificateChoiceSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private Boolean shareMdClientIpAddress; + private String initialCallbackUrl; + + /** + * Constructs a new DeviceLinkCertificateRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the initial callback URL for the device link session. + * This URL is used to redirect the user after the session is initialized. + * + * @param initialCallbackUrl the initial callback URL + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Starts a device link-based certificate choice session and returns the session response. + * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, + * which can be used by the Relying Party to manage and verify the session independently. + * + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. + * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. + * @throws UnprocessableSmartIdResponseException if the response is missing required fields. + */ + public DeviceLinkSessionResponse initCertificateChoice() { + validateRequestParameters(); + DeviceLinkCertificateChoiceSessionRequest deviceLinkCertificateChoiceSessionRequest = createCertificateRequest(); + DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(deviceLinkCertificateChoiceSessionRequest); + validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); + return deviceLinkCertificateChoiceSessionResponse; + } + + private void validateRequestParameters() { + if (isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must have length between 1 and 30 characters"); + } + validateInitialCallbackUrl(); + } + + private DeviceLinkCertificateChoiceSessionRequest createCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl + ); + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); + } + + if (deviceLinkCertificateChoiceSessionResponse.deviceLinkBase() == null + || deviceLinkCertificateChoiceSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 56687739..8a997ec5 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -1,356 +1,356 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a device-link signature session - */ -public class DeviceLinkSignatureSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private SemanticsIdentifier semanticsIdentifier; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private List interactions; - private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private String initialCallbackUrl; - private DigestInput digestInput; - - private DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest; - - /** - * Constructs a new Smart-ID signature request builder with the given connector. - * - * @param connector the connector - */ - public DeviceLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the semantics identifier. - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the interactions for device-link signature. - * - * @param interactions the interactions - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the data to be signed. - *

- * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - * - * @param signableData the data to be signed - * @return this builder instance - */ - public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (this.digestInput != null && this.digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash."); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the hash to be signed in the signature protocol. - *

- * The provided {@link SignableHash} must contain a valid hash value and hash type, - * which will be used as the digest in the signing request. - * - * @param signableHash the hash data to be signed - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (this.digestInput != null && this.digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData."); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sets the initial callback URL. - *

- * This URL is used to redirect the user after the signature session is completed. - * - * @param initialCallbackUrl the initial callback URL - * @return this builder instance - */ - public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Sends the signature request and initiates a device link based signature session. - *

- * There are two supported ways to start the signature session: - *

    - *
  • with a document number by using {@link #withDocumentNumber(String)}
  • - *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
- * - * @return a {@link DeviceLinkSessionResponse} containing session details such as - * session ID, session token, session secret and device link base URL. - * @throws SmartIdRequestSetupException if request parameters are invalid - * @throws SmartIdClientException if digest calculation fails or if both signableData and signableHash are missing - * @throws UnprocessableSmartIdResponseException if the response is missing required fields - */ - public DeviceLinkSessionResponse initSignatureSession() { - validateRequestParameters(); - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = createSignatureSessionRequest(); - DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(deviceLinkSignatureSessionRequest); - validateResponseParameters(deviceLinkSignatureSessionResponse); - this.deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequest; - return deviceLinkSignatureSessionResponse; - } - - /** - * Gets the DeviceLinkSignatureSessionRequest that was used to initiate the signature session. - *

- * This method can only be called after {@link #initSignatureSession()} has been invoked. - * - * @return the signature request that was used to initiate the session - * @throws SmartIdClientException if called before initSignatureSession() - */ - public DeviceLinkSignatureSessionRequest getSignatureSessionRequest() { - if (deviceLinkSignatureSessionRequest == null) { - throw new SmartIdClientException("Signature session has not been initiated yet"); - } - return deviceLinkSignatureSessionRequest; - } - - private DeviceLinkSessionResponse initSignatureSession(DeviceLinkSignatureSessionRequest request) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (!StringUtil.isEmpty(documentNumber)) { - return connector.initDeviceLinkSignature(request, documentNumber); - } else if (semanticsIdentifier != null) { - return connector.initDeviceLinkSignature(request, semanticsIdentifier); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"); - } - } - - private DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new DeviceLinkSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - nonce != null ? nonce : null, - capabilities, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - initialCallbackUrl); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); - } - validateInteractions(); - validateInitialCallbackUrl(); - - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters."); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); - } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); - } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); - } - if (deviceLinkSignatureSessionResponse.deviceLinkBase() == null || deviceLinkSignatureSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } - +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a device-link signature session + */ +public class DeviceLinkSignatureSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List interactions; + private Boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private String initialCallbackUrl; + private DigestInput digestInput; + + private DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public DeviceLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the interactions for device-link signature. + * + * @param interactions the interactions + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

+ * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + * + * @param signableData the data to be signed + * @return this builder instance + */ + public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash."); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

+ * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + * + * @param signableHash the hash data to be signed + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData."); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sets the initial callback URL. + *

+ * This URL is used to redirect the user after the signature session is completed. + * + * @param initialCallbackUrl the initial callback URL + * @return this builder instance + */ + public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Sends the signature request and initiates a device link based signature session. + *

+ * There are two supported ways to start the signature session: + *

    + *
  • with a document number by using {@link #withDocumentNumber(String)}
  • + *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
+ * + * @return a {@link DeviceLinkSessionResponse} containing session details such as + * session ID, session token, session secret and device link base URL. + * @throws SmartIdRequestSetupException if request parameters are invalid + * @throws SmartIdClientException if digest calculation fails or if both signableData and signableHash are missing + * @throws UnprocessableSmartIdResponseException if the response is missing required fields + */ + public DeviceLinkSessionResponse initSignatureSession() { + validateRequestParameters(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = createSignatureSessionRequest(); + DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(deviceLinkSignatureSessionRequest); + validateResponseParameters(deviceLinkSignatureSessionResponse); + this.deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequest; + return deviceLinkSignatureSessionResponse; + } + + /** + * Gets the DeviceLinkSignatureSessionRequest that was used to initiate the signature session. + *

+ * This method can only be called after {@link #initSignatureSession()} has been invoked. + * + * @return the signature request that was used to initiate the session + * @throws SmartIdClientException if called before initSignatureSession() + */ + public DeviceLinkSignatureSessionRequest getSignatureSessionRequest() { + if (deviceLinkSignatureSessionRequest == null) { + throw new SmartIdClientException("Signature session has not been initiated yet"); + } + return deviceLinkSignatureSessionRequest; + } + + private DeviceLinkSessionResponse initSignatureSession(DeviceLinkSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (!StringUtil.isEmpty(documentNumber)) { + return connector.initDeviceLinkSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initDeviceLinkSignature(request, semanticsIdentifier); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"); + } + } + + private DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + return new DeviceLinkSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce != null ? nonce : null, + capabilities, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } + validateInteractions(); + validateInitialCallbackUrl(); + + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters."); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); + } + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); + } + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); + } + if (deviceLinkSignatureSessionResponse.deviceLinkBase() == null || deviceLinkSignatureSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } + } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DeviceLinkType.java b/src/main/java/ee/sk/smartid/DeviceLinkType.java index 0f8448aa..efd71eed 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkType.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkType.java @@ -1,63 +1,63 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Representation types for different device link flows. - */ -public enum DeviceLinkType { - - /** - * QR-code (cross-device) flow. - */ - QR_CODE("QR"), - - /** - * Web2App (same-device) flow. - */ - WEB_2_APP("Web2App"), - - /** - * App2App (same-device) flow. - */ - APP_2_APP("App2App"); - - private final String value; - - DeviceLinkType(String value) { - this.value = value; - } - - /** - * Provides the device link type as value that can be used in the Smart-ID API - * - * @return code representing the device link type - */ - public String getValue() { - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Representation types for different device link flows. + */ +public enum DeviceLinkType { + + /** + * QR-code (cross-device) flow. + */ + QR_CODE("QR"), + + /** + * Web2App (same-device) flow. + */ + WEB_2_APP("Web2App"), + + /** + * App2App (same-device) flow. + */ + APP_2_APP("App2App"); + + private final String value; + + DeviceLinkType(String value) { + this.value = value; + } + + /** + * Provides the device link type as value that can be used in the Smart-ID API + * + * @return code representing the device link type + */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java index a8ace55e..28e067f1 100644 --- a/src/main/java/ee/sk/smartid/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/DigestCalculator.java @@ -1,61 +1,61 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for calculating digests using specified hash algorithms. - */ -public final class DigestCalculator { - - private DigestCalculator() { - } - - /** - * Calculates the digest of the provided data using the specified hash algorithm. - * - * @param dataToDigest The data to be hashed. - * @param hashAlgorithm The hash algorithm to use. - * @return The calculated digest as a byte array. - * @throws SmartIdClientException If there is an issue with the digest calculation. - */ - public static byte[] calculateDigest(byte[] dataToDigest, HashAlgorithm hashAlgorithm) { - if (hashAlgorithm == null) { - throw new SmartIdClientException("Parameter 'hashAlgorithm' must be set"); - } - try { - MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName()); - return digest.digest(dataToDigest); - } catch (NoSuchAlgorithmException ex) { - throw new SmartIdClientException("Problem with digest calculation.", ex); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for calculating digests using specified hash algorithms. + */ +public final class DigestCalculator { + + private DigestCalculator() { + } + + /** + * Calculates the digest of the provided data using the specified hash algorithm. + * + * @param dataToDigest The data to be hashed. + * @param hashAlgorithm The hash algorithm to use. + * @return The calculated digest as a byte array. + * @throws SmartIdClientException If there is an issue with the digest calculation. + */ + public static byte[] calculateDigest(byte[] dataToDigest, HashAlgorithm hashAlgorithm) { + if (hashAlgorithm == null) { + throw new SmartIdClientException("Parameter 'hashAlgorithm' must be set"); + } + try { + MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName()); + return digest.digest(dataToDigest); + } catch (NoSuchAlgorithmException ex) { + throw new SmartIdClientException("Problem with digest calculation.", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DigestInput.java b/src/main/java/ee/sk/smartid/DigestInput.java index bf74c745..c2a7721a 100644 --- a/src/main/java/ee/sk/smartid/DigestInput.java +++ b/src/main/java/ee/sk/smartid/DigestInput.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Represents data to be signed. - *

- * Digest for signing can be provided either as a pre-calculated hash value {@link SignableHash} or as raw data to be hashed {@link SignableData}. - *

- * Implementers must make sure that the getDigestInBase64() method returns the digest of the data to be in Base64-encoded format and the hashAlgorithm() - * return the correct hash algorithm used for calculating the digest or to be used for hashing the raw data. - */ -public interface DigestInput { - - /** - * Gets the digest in Base64-encoded string. - * - * @return the digest in base64 encoding - */ - String getDigestInBase64(); - - /** - * Gets the hash algorithm used for calculating the digest or to be used for hashing the raw data. - * - * @return the hash algorithm - */ - HashAlgorithm hashAlgorithm(); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Represents data to be signed. + *

+ * Digest for signing can be provided either as a pre-calculated hash value {@link SignableHash} or as raw data to be hashed {@link SignableData}. + *

+ * Implementers must make sure that the getDigestInBase64() method returns the digest of the data to be in Base64-encoded format and the hashAlgorithm() + * return the correct hash algorithm used for calculating the digest or to be used for hashing the raw data. + */ +public interface DigestInput { + + /** + * Gets the digest in Base64-encoded string. + * + * @return the digest in base64 encoding + */ + String getDigestInBase64(); + + /** + * Gets the hash algorithm used for calculating the digest or to be used for hashing the raw data. + * + * @return the hash algorithm + */ + HashAlgorithm hashAlgorithm(); +} diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index 26f323c1..d96e44df 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -1,97 +1,97 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.UserAccountException; -import ee.sk.smartid.exception.UserActionException; -import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; -import ee.sk.smartid.exception.permanent.ProtocolFailureException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdServerException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.util.StringUtil; - -/** - * Handles session status results that end as completed but with an error - */ -public class ErrorResultHandler { - - /** - * Handles the session result and throws an appropriate exception - * - * @param sessionResult the session result to handle - * @throws SmartIdClientException when input parameter sessionResult is null - * @throws UserActionException sub-exceptions based on end result - * @throws UserAccountException sub-exceptions based on end result - * @throws ProtocolFailureException when there was an error in the process (e.g. schema name is incorrect) - * @throws ExpectedLinkedSessionException when different session type was started than expected - * @throws SmartIdServerException when technical error occurred on server side - * @throws UnprocessableSmartIdResponseException when unexpected end result was received - */ - public static void handle(SessionResult sessionResult) { - if (sessionResult == null) { - throw new SmartIdClientException("Parameter 'sessionResult' is not provided"); - } - switch (sessionResult.getEndResult()) { - case "USER_REFUSED" -> throw new UserRefusedException(); - case "TIMEOUT" -> throw new SessionTimeoutException(); - case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); - case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); - case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); - case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); - case "USER_REFUSED_INTERACTION" -> handleUserRefusedInteraction(sessionResult); - case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); - case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); - case "SERVER_ERROR" -> throw new SmartIdServerException(); - case "ACCOUNT_UNUSABLE" -> throw new UserAccountUnusableException(); - default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); - } - } - - private static void handleUserRefusedInteraction(SessionResult sessionResult) { - if (sessionResult.getDetails() == null || StringUtil.isEmpty(sessionResult.getDetails().getInteraction())) { - throw new UnprocessableSmartIdResponseException("Details for refused interaction are missing"); - } - switch (sessionResult.getDetails().getInteraction()) { - case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); - case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); - case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); - default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.UserAccountException; +import ee.sk.smartid.exception.UserActionException; +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.util.StringUtil; + +/** + * Handles session status results that end as completed but with an error + */ +public class ErrorResultHandler { + + /** + * Handles the session result and throws an appropriate exception + * + * @param sessionResult the session result to handle + * @throws SmartIdClientException when input parameter sessionResult is null + * @throws UserActionException sub-exceptions based on end result + * @throws UserAccountException sub-exceptions based on end result + * @throws ProtocolFailureException when there was an error in the process (e.g. schema name is incorrect) + * @throws ExpectedLinkedSessionException when different session type was started than expected + * @throws SmartIdServerException when technical error occurred on server side + * @throws UnprocessableSmartIdResponseException when unexpected end result was received + */ + public static void handle(SessionResult sessionResult) { + if (sessionResult == null) { + throw new SmartIdClientException("Parameter 'sessionResult' is not provided"); + } + switch (sessionResult.getEndResult()) { + case "USER_REFUSED" -> throw new UserRefusedException(); + case "TIMEOUT" -> throw new SessionTimeoutException(); + case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); + case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); + case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); + case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); + case "USER_REFUSED_INTERACTION" -> handleUserRefusedInteraction(sessionResult); + case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); + case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); + case "SERVER_ERROR" -> throw new SmartIdServerException(); + case "ACCOUNT_UNUSABLE" -> throw new UserAccountUnusableException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); + } + } + + private static void handleUserRefusedInteraction(SessionResult sessionResult) { + if (sessionResult.getDetails() == null || StringUtil.isEmpty(sessionResult.getDetails().getInteraction())) { + throw new UnprocessableSmartIdResponseException("Details for refused interaction are missing"); + } + switch (sessionResult.getDetails().getInteraction()) { + case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); + case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); + case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); + } + } +} diff --git a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java index be68443e..5d77135e 100644 --- a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java +++ b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java @@ -1,224 +1,224 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.PKIXCertPathValidatorResult; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for TrustedCACertStore that loads trust anchors and intermediate CA certificates from specified keystore files. - *

- * The builder allows configuration of trust anchor and intermediate CA keystore paths and passwords. - */ -public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { - - private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); - - private String trustAnchorTruststorePath = "/sid_trust_anchor_certificates.jks"; - private String trustAnchorTruststorePassword = "changeit"; - - private String intermediateCATruststorePath = "/trusted_certificates.jks"; - private String trustedCaTruststorePassword = "changeit"; - - private boolean ocspEnabled = false; // TODO - 03.07.25: set to true if OCSP validations is working - private X509Certificate ocspValidationCert; // TODO - 02.07.25: implement reading from a file system - - /** - * Sets the path to the trust anchor keystore file. - * - * @param path the path to the trust anchor keystore file - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withTrustAnchorTruststorePath(String path) { - this.trustAnchorTruststorePath = path; - return this; - } - - /** - * Sets the password for the trust anchor keystore. - * - * @param password the password for the trust anchor keystore - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withTrustAnchorTruststorePassword(String password) { - this.trustAnchorTruststorePassword = password; - return this; - } - - /** - * Sets the path to the intermediate CA keystore file. - * - * @param path the path to the trusted CA keystore file - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withIntermediateCATruststorePath(String path) { - this.intermediateCATruststorePath = path; - return this; - } - - /** - * Sets the password for the trusted CA keystore. - * - * @param password the password for the trusted CA keystore - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withIntermediateCATruststorePassword(String password) { - this.trustedCaTruststorePassword = password; - return this; - } - - /** - * Enables or disables OCSP (Online Certificate Status Protocol) for certificate validation. - * - * @param enabled true to enable OCSP, false to disable it - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { - this.ocspEnabled = enabled; - return this; - } - - /** - * Builds a new TrustedCAStoreImpl instance with the specified configuration. - * - * @return a new TrustedCAStoreImpl instances - */ - @Override - public TrustedCACertStore build() { - if (!ocspEnabled) { - logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); - } else { - throw new UnsupportedOperationException("OCSP validation does not work yet, will be implemented later"); - } - Set trustAnchors = loadTrustAnchors(); - List trustedCACertificates = loadValidatedIntermediateCACertificates(trustAnchors); - return new DefaultTrustedCACertStore(trustAnchors, trustedCACertificates, ocspEnabled); - } - - private Set loadTrustAnchors() { - if (StringUtil.isEmpty(trustAnchorTruststorePath)) { - throw new SmartIdClientException("Trust anchor truststore path must be set"); - } - if (StringUtil.isEmpty(trustAnchorTruststorePassword)) { - throw new SmartIdClientException("Trust anchor truststore password must be set"); - } - try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(trustAnchorTruststorePath)) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, trustAnchorTruststorePassword.toCharArray()); - Enumeration aliases = keystore.aliases(); - Set trustAnchors = new HashSet<>(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - certificate.verify(certificate.getPublicKey()); - certificate.checkValidity(); - trustAnchors.add(new TrustAnchor(certificate, null)); - } - return trustAnchors; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing trust anchor certificate", e); - throw new SmartIdClientException("Error initializing trust anchor certificate", e); - } catch (SignatureException | InvalidKeyException | NoSuchProviderException ex) { - throw new SmartIdClientException("Failed to verify trust anchor certificate", ex); - } - } - - private List loadValidatedIntermediateCACertificates(Set trustAnchors) { - if (StringUtil.isEmpty(intermediateCATruststorePath)) { - throw new SmartIdClientException("Intermediate CA certificate truststore path must be set"); - } - if (StringUtil.isEmpty(trustedCaTruststorePassword)) { - throw new SmartIdClientException("Intermediate CA certificate truststore password must be set"); - } - try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(intermediateCATruststorePath)) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, trustedCaTruststorePassword.toCharArray()); - Enumeration aliases = keystore.aliases(); - List trustedCACertificates = new ArrayList<>(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - certificate.checkValidity(); - validateCertificate(trustAnchors, certificate); - trustedCACertificates.add(certificate); - } - return trustedCACertificates; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing intermediate CA certificates", e); - throw new SmartIdClientException("Error initializing intermediate CA certificates", e); - } - } - - private void validateCertificate(Set trustAnchors, X509Certificate x509Certificates) { - try { - var cf = CertificateFactory.getInstance("X.509"); - CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); - var pkixParameters = new PKIXParameters(trustAnchors); - pkixParameters.setRevocationEnabled(ocspEnabled); - var certPathValidator = CertPathValidator.getInstance("PKIX"); - var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); - var trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); - } catch (GeneralSecurityException ex) { - logger.debug("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); - throw new SmartIdClientException("Validating intermediate CA failed", ex); - } - } - - private String getCNValue(X509Certificate certificate) { - String subjectDN = certificate.getSubjectX500Principal().getName(); - return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for TrustedCACertStore that loads trust anchors and intermediate CA certificates from specified keystore files. + *

+ * The builder allows configuration of trust anchor and intermediate CA keystore paths and passwords. + */ +public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); + + private String trustAnchorTruststorePath = "/sid_trust_anchor_certificates.jks"; + private String trustAnchorTruststorePassword = "changeit"; + + private String intermediateCATruststorePath = "/trusted_certificates.jks"; + private String trustedCaTruststorePassword = "changeit"; + + private boolean ocspEnabled = false; // TODO - 03.07.25: set to true if OCSP validations is working + private X509Certificate ocspValidationCert; // TODO - 02.07.25: implement reading from a file system + + /** + * Sets the path to the trust anchor keystore file. + * + * @param path the path to the trust anchor keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePath(String path) { + this.trustAnchorTruststorePath = path; + return this; + } + + /** + * Sets the password for the trust anchor keystore. + * + * @param password the password for the trust anchor keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePassword(String password) { + this.trustAnchorTruststorePassword = password; + return this; + } + + /** + * Sets the path to the intermediate CA keystore file. + * + * @param path the path to the trusted CA keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePath(String path) { + this.intermediateCATruststorePath = path; + return this; + } + + /** + * Sets the password for the trusted CA keystore. + * + * @param password the password for the trusted CA keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePassword(String password) { + this.trustedCaTruststorePassword = password; + return this; + } + + /** + * Enables or disables OCSP (Online Certificate Status Protocol) for certificate validation. + * + * @param enabled true to enable OCSP, false to disable it + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instances + */ + @Override + public TrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("OCSP validation does not work yet, will be implemented later"); + } + Set trustAnchors = loadTrustAnchors(); + List trustedCACertificates = loadValidatedIntermediateCACertificates(trustAnchors); + return new DefaultTrustedCACertStore(trustAnchors, trustedCACertificates, ocspEnabled); + } + + private Set loadTrustAnchors() { + if (StringUtil.isEmpty(trustAnchorTruststorePath)) { + throw new SmartIdClientException("Trust anchor truststore path must be set"); + } + if (StringUtil.isEmpty(trustAnchorTruststorePassword)) { + throw new SmartIdClientException("Trust anchor truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(trustAnchorTruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustAnchorTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + Set trustAnchors = new HashSet<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.verify(certificate.getPublicKey()); + certificate.checkValidity(); + trustAnchors.add(new TrustAnchor(certificate, null)); + } + return trustAnchors; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing trust anchor certificate", e); + throw new SmartIdClientException("Error initializing trust anchor certificate", e); + } catch (SignatureException | InvalidKeyException | NoSuchProviderException ex) { + throw new SmartIdClientException("Failed to verify trust anchor certificate", ex); + } + } + + private List loadValidatedIntermediateCACertificates(Set trustAnchors) { + if (StringUtil.isEmpty(intermediateCATruststorePath)) { + throw new SmartIdClientException("Intermediate CA certificate truststore path must be set"); + } + if (StringUtil.isEmpty(trustedCaTruststorePassword)) { + throw new SmartIdClientException("Intermediate CA certificate truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(intermediateCATruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustedCaTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + List trustedCACertificates = new ArrayList<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.checkValidity(); + validateCertificate(trustAnchors, certificate); + trustedCACertificates.add(certificate); + } + return trustedCACertificates; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing intermediate CA certificates", e); + throw new SmartIdClientException("Error initializing intermediate CA certificates", e); + } + } + + private void validateCertificate(Set trustAnchors, X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.debug("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/FlowType.java b/src/main/java/ee/sk/smartid/FlowType.java index 984a5114..5ae94f56 100644 --- a/src/main/java/ee/sk/smartid/FlowType.java +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -1,97 +1,97 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents the flow types that user used to complete the authentication or signing. - */ -public enum FlowType { - - /** - * QR-code (cross-device) flow. User scanned a QR-code with the Smart-ID app. - */ - QR("QR"), - - /** - * Web2App (same-device) flow. User clicked on a link in the browser on a mobile device - * and confirmed with the Smart-ID app. - */ - WEB2APP("Web2App"), - - /** - * App2App (same-device) flow. User clicked on a link in the app on a mobile device - * and confirmed with the Smart-ID app. - */ - APP2APP("App2App"), - - /** - * Notification flow. User received a push-notification and confirmed with the Smart-ID app. - */ - NOTIFICATION("Notification"); - - private final String description; - - FlowType(String description) { - this.description = description; - } - - /*** - * Gets the value used in the Smart-ID API to represent the flow type. - * - * @return the flow type description - */ - public String getDescription() { - return description; - } - - /** - * Checks if the provided flow type is supported. - * - * @param flowType the flow type to check - * @return true if the flow type is supported, false otherwise - */ - public static boolean isSupported(String flowType) { - return Arrays.stream(FlowType.values()) - .anyMatch(f -> f.getDescription().equals(flowType)); - } - - /** - * Converts a string representation of a flow type to the corresponding FlowType enum value. - * - * @param flowType the string representation of the flow type - * @return the corresponding FlowType enum value - * @throws IllegalArgumentException if the provided flow type is not valid - */ - public static FlowType fromString(String flowType) { - return Arrays.stream(FlowType.values()) - .filter(f -> f.getDescription().equals(flowType)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid flowType value: " + flowType)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents the flow types that user used to complete the authentication or signing. + */ +public enum FlowType { + + /** + * QR-code (cross-device) flow. User scanned a QR-code with the Smart-ID app. + */ + QR("QR"), + + /** + * Web2App (same-device) flow. User clicked on a link in the browser on a mobile device + * and confirmed with the Smart-ID app. + */ + WEB2APP("Web2App"), + + /** + * App2App (same-device) flow. User clicked on a link in the app on a mobile device + * and confirmed with the Smart-ID app. + */ + APP2APP("App2App"), + + /** + * Notification flow. User received a push-notification and confirmed with the Smart-ID app. + */ + NOTIFICATION("Notification"); + + private final String description; + + FlowType(String description) { + this.description = description; + } + + /*** + * Gets the value used in the Smart-ID API to represent the flow type. + * + * @return the flow type description + */ + public String getDescription() { + return description; + } + + /** + * Checks if the provided flow type is supported. + * + * @param flowType the flow type to check + * @return true if the flow type is supported, false otherwise + */ + public static boolean isSupported(String flowType) { + return Arrays.stream(FlowType.values()) + .anyMatch(f -> f.getDescription().equals(flowType)); + } + + /** + * Converts a string representation of a flow type to the corresponding FlowType enum value. + * + * @param flowType the string representation of the flow type + * @return the corresponding FlowType enum value + * @throws IllegalArgumentException if the provided flow type is not valid + */ + public static FlowType fromString(String flowType) { + return Arrays.stream(FlowType.values()) + .filter(f -> f.getDescription().equals(flowType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid flowType value: " + flowType)); + } +} diff --git a/src/main/java/ee/sk/smartid/HashAlgorithm.java b/src/main/java/ee/sk/smartid/HashAlgorithm.java index f7e74a9d..959f337c 100644 --- a/src/main/java/ee/sk/smartid/HashAlgorithm.java +++ b/src/main/java/ee/sk/smartid/HashAlgorithm.java @@ -1,102 +1,102 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; -import java.util.Optional; - -/** - * Represents hash algorithms used for hashing the data to be signed. - *

- * * The algorithm name is the name used in the Smart-ID API. - * * The octet length represents salt length in bytes (octets) produced by the hash algorithm. - */ -public enum HashAlgorithm { - - /** - * SHA-256 - produces 32 bytes (256 bit) hash - */ - SHA_256("SHA-256", 32), - /** - * SHA-384 - produces 48 bytes (384 bit) hash - */ - SHA_384("SHA-384", 48), - /** - * SHA-384 - produces 64 bytes (512 bit) hash - */ - SHA_512("SHA-512", 64), - /** - * SHA3-256 - produces 32 bytes (256 bit) hash - */ - SHA3_256("SHA3-256", 32), - /** - * SHA3-384 - produces 48 bytes (384 bit) hash - */ - SHA3_384("SHA3-384", 48), - /** - * SHA3-384 - produces 64 bytes (512 bit) hash - */ - SHA3_512("SHA3-512", 64); - - private final String algorithmName; - private final int octetLength; - - HashAlgorithm(String algorithmName, int octetLength) { - this.algorithmName = algorithmName; - this.octetLength = octetLength; - } - - /** - * Gets the name of the algorithm as used in the Smart-ID API. - * - * @return the algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Gets the length of the hash in bytes (octets). - * - * @return the octet length - */ - public int getOctetLength() { - return octetLength; - } - - /** - * Find HashAlgorithm by its name. - * - * @param input the name of the algorithm - * @return an Optional containing the HashAlgorithm if found, or an empty Optional if not found - */ - public static Optional fromString(String input) { - return Arrays.stream(HashAlgorithm.values()) - .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) - .findFirst(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents hash algorithms used for hashing the data to be signed. + *

+ * * The algorithm name is the name used in the Smart-ID API. + * * The octet length represents salt length in bytes (octets) produced by the hash algorithm. + */ +public enum HashAlgorithm { + + /** + * SHA-256 - produces 32 bytes (256 bit) hash + */ + SHA_256("SHA-256", 32), + /** + * SHA-384 - produces 48 bytes (384 bit) hash + */ + SHA_384("SHA-384", 48), + /** + * SHA-384 - produces 64 bytes (512 bit) hash + */ + SHA_512("SHA-512", 64), + /** + * SHA3-256 - produces 32 bytes (256 bit) hash + */ + SHA3_256("SHA3-256", 32), + /** + * SHA3-384 - produces 48 bytes (384 bit) hash + */ + SHA3_384("SHA3-384", 48), + /** + * SHA3-384 - produces 64 bytes (512 bit) hash + */ + SHA3_512("SHA3-512", 64); + + private final String algorithmName; + private final int octetLength; + + HashAlgorithm(String algorithmName, int octetLength) { + this.algorithmName = algorithmName; + this.octetLength = octetLength; + } + + /** + * Gets the name of the algorithm as used in the Smart-ID API. + * + * @return the algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Gets the length of the hash in bytes (octets). + * + * @return the octet length + */ + public int getOctetLength() { + return octetLength; + } + + /** + * Find HashAlgorithm by its name. + * + * @param input the name of the algorithm + * @return an Optional containing the HashAlgorithm if found, or an empty Optional if not found + */ + public static Optional fromString(String input) { + return Arrays.stream(HashAlgorithm.values()) + .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) + .findFirst(); + } +} diff --git a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java index 67785957..0d2ca035 100644 --- a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -1,280 +1,280 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for initializing a linked notification signature session request. - * Must follow an anonymous device link certificate choice session - */ -public class LinkedNotificationSignatureSessionRequestBuilder { - - private final SmartIdConnector smartIdConnector; - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private DigestInput digestInput; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private String linkedSessionID; - private List interactions; - private CertificateLevel certificateLevel; - private String nonce; - private Boolean shareIpAddress; - private Set capabilities; - - /** - * Initializes the builder with the given Smart ID connector. - * - * @param smartIdConnector the Smart-ID connector - */ - public LinkedNotificationSignatureSessionRequestBuilder(SmartIdConnector smartIdConnector) { - this.smartIdConnector = smartIdConnector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the signable data. - * - * @param signableData the data to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with SignableHash - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (digestInput != null && digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableHash"); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the signable hash. - * - * @param signableHash the hash to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with SignableData - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (digestInput != null && digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableData"); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm The signature algorithm - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the linked session ID. - * - * @param linkedSessionID the session ID from the device link certificate choice session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withLinkedSessionID(String linkedSessionID) { - this.linkedSessionID = linkedSessionID; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the interactions. - * - * @param interactions list of interactions to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the mobile device's IP address with the relying party. - * - * @param shareIpAddress true to share the IP address, false otherwise - * @return this - */ - public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareIpAddress) { - this.shareIpAddress = shareIpAddress; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Initializes the linked notification signature session. - * - * @return The linked signature session response - * @throws SmartIdRequestSetupException when any required parameter is missing or invalid - * @throws UnprocessableSmartIdResponseException when server response is missing required fields - */ - public LinkedSignatureSessionResponse initSignatureSession() { - validateRequestParameters(); - LinkedSignatureSessionRequest request = createSessionRequest(); - LinkedSignatureSessionResponse linkedSignatureSessionResponse = smartIdConnector.initLinkedNotificationSignature(request, documentNumber); - validateResponse(linkedSignatureSessionResponse); - return linkedSignatureSessionResponse; - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (StringUtil.isEmpty(documentNumber)) { - throw new SmartIdRequestSetupException("Value for 'documentNumber' cannot be empty"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (StringUtil.isEmpty(linkedSessionID)) { - throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' must be 1-30 characters long"); - } - if (interactions == null || interactions.isEmpty()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private LinkedSignatureSessionRequest createSessionRequest() { - var rawDigestParams = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new LinkedSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - rawDigestParams, - linkedSessionID, - nonce, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, - capabilities); - } - - private void validateResponse(LinkedSignatureSessionResponse linkedSignatureSessionResponse) { - if (StringUtil.isEmpty(linkedSignatureSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Linked notification-base signature session response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for initializing a linked notification signature session request. + * Must follow an anonymous device link certificate choice session + */ +public class LinkedNotificationSignatureSessionRequestBuilder { + + private final SmartIdConnector smartIdConnector; + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private DigestInput digestInput; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private String linkedSessionID; + private List interactions; + private CertificateLevel certificateLevel; + private String nonce; + private Boolean shareIpAddress; + private Set capabilities; + + /** + * Initializes the builder with the given Smart ID connector. + * + * @param smartIdConnector the Smart-ID connector + */ + public LinkedNotificationSignatureSessionRequestBuilder(SmartIdConnector smartIdConnector) { + this.smartIdConnector = smartIdConnector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the signable data. + * + * @param signableData the data to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableHash + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (digestInput != null && digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableHash"); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the signable hash. + * + * @param signableHash the hash to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableData + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (digestInput != null && digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableData"); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm The signature algorithm + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the linked session ID. + * + * @param linkedSessionID the session ID from the device link certificate choice session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withLinkedSessionID(String linkedSessionID) { + this.linkedSessionID = linkedSessionID; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the interactions. + * + * @param interactions list of interactions to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the mobile device's IP address with the relying party. + * + * @param shareIpAddress true to share the IP address, false otherwise + * @return this + */ + public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareIpAddress) { + this.shareIpAddress = shareIpAddress; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Initializes the linked notification signature session. + * + * @return The linked signature session response + * @throws SmartIdRequestSetupException when any required parameter is missing or invalid + * @throws UnprocessableSmartIdResponseException when server response is missing required fields + */ + public LinkedSignatureSessionResponse initSignatureSession() { + validateRequestParameters(); + LinkedSignatureSessionRequest request = createSessionRequest(); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = smartIdConnector.initLinkedNotificationSignature(request, documentNumber); + validateResponse(linkedSignatureSessionResponse); + return linkedSignatureSessionResponse; + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (StringUtil.isEmpty(documentNumber)) { + throw new SmartIdRequestSetupException("Value for 'documentNumber' cannot be empty"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (StringUtil.isEmpty(linkedSessionID)) { + throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must be 1-30 characters long"); + } + if (interactions == null || interactions.isEmpty()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private LinkedSignatureSessionRequest createSessionRequest() { + var rawDigestParams = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + return new LinkedSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + rawDigestParams, + linkedSessionID, + nonce, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, + capabilities); + } + + private void validateResponse(LinkedSignatureSessionResponse linkedSignatureSessionResponse) { + if (StringUtil.isEmpty(linkedSignatureSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Linked notification-base signature session response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java index dad59496..d0800ca3 100644 --- a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -1,80 +1,80 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents mask algorithm in the response and the value used in recrating the signature. - */ -public enum MaskGenAlgorithm { - - /** - * id-mgf1 is used in the Smart-ID API and MGF1 is the name used in the Java Cryptography API. - */ - ID_MGF1("id-mgf1", "MGF1"); - - private final String algorithmName; - private final String mgfName; - - MaskGenAlgorithm(String algorithmName, String mgfName) { - this.algorithmName = algorithmName; - this.mgfName = mgfName; - } - - /** - * Gets the algorithm name used by the Smart-ID API. - * - * @return the algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Gets the MGF name used in the Java Cryptography API. - * - * @return the MGF name - */ - public String getMgfName() { - return mgfName; - } - - /** - * Converts a string to the corresponding MaskGenAlgorithm enum value. - * - * @param maskGenAlgorithm the string representation of the mask generation algorithm - * @return the corresponding MaskGenAlgorithm enum value - * @throws IllegalArgumentException if the provided string does not match any enum value - */ - public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { - return Arrays.stream(MaskGenAlgorithm.values()) - .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid maskGenAlgorithm value: " + maskGenAlgorithm)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents mask algorithm in the response and the value used in recrating the signature. + */ +public enum MaskGenAlgorithm { + + /** + * id-mgf1 is used in the Smart-ID API and MGF1 is the name used in the Java Cryptography API. + */ + ID_MGF1("id-mgf1", "MGF1"); + + private final String algorithmName; + private final String mgfName; + + MaskGenAlgorithm(String algorithmName, String mgfName) { + this.algorithmName = algorithmName; + this.mgfName = mgfName; + } + + /** + * Gets the algorithm name used by the Smart-ID API. + * + * @return the algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Gets the MGF name used in the Java Cryptography API. + * + * @return the MGF name + */ + public String getMgfName() { + return mgfName; + } + + /** + * Converts a string to the corresponding MaskGenAlgorithm enum value. + * + * @param maskGenAlgorithm the string representation of the mask generation algorithm + * @return the corresponding MaskGenAlgorithm enum value + * @throws IllegalArgumentException if the provided string does not match any enum value + */ + public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { + return Arrays.stream(MaskGenAlgorithm.values()) + .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid maskGenAlgorithm value: " + maskGenAlgorithm)); + } +} diff --git a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java index 52132b42..fd42eb28 100644 --- a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java @@ -1,56 +1,56 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the signature certificate is a nonqualified Smart-ID certificate and can be used for digital signing. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile - */ -public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { - - @Override - public void validate(X509Certificate certificate) { - NonQualifiedSmartIdCertificateValidator.validate(certificate); - validateCertificateCanBeUsedForSigning(certificate); - } - - private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { - if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a nonqualified Smart-ID certificate and can be used for digital signing. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + */ +public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + @Override + public void validate(X509Certificate certificate) { + NonQualifiedSmartIdCertificateValidator.validate(certificate); + validateCertificateCanBeUsedForSigning(certificate); + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java index 0efb494e..8051558b 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -1,189 +1,189 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates notification-based authentication session status - */ -public class NotificationAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; - - /** - * Creates an instance of {@link NotificationAuthenticationResponseValidator} - * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value - * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validators based on certificate level - */ - public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; - } - - /** - * Creates an instance of {@link NotificationAuthenticationResponseValidator} using {@link CertificateValidator} - * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @return a new instance of {@link NotificationAuthenticationResponseValidator} - */ - public static NotificationAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { - return new NotificationAuthenticationResponseValidator(certificateValidator, - new AuthenticationResponseMapperImpl(), - new SignatureValueValidatorImpl(), - new AuthenticationCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name used in the QR-code or device link - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, schemaName, null); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - *

- * Should only be used for QR-code or notification-based authentication validation - * - * @param sessionStatus the authentication session status to be validated - * @param authenticationSessionRequest the authentication session request that was used to start the session - * @param schemaName the schema name used in the QR-code or device link - * @param brokeredRpName the brokered relying party name - * @return authentication identity containing details about the authenticated user - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - private void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - private AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { - return authenticationSessionRequest.certificateLevel() == null - ? AuthenticationCertificateLevel.QUALIFIED - : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - String[] payload = { - schemaName, - SignatureProtocol.ACSP_V2.name(), - authenticationResponse.getServerRandom(), - authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), - StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - toBase64(authenticationSessionRequest.relyingPartyName()), - StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), - authenticationResponse.getInteractionTypeUsed(), - "", - authenticationResponse.getFlowType().getDescription() - }; - return String - .join("|", payload) - .getBytes(StandardCharsets.UTF_8); - } - - private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private static String toBase64(String input) { - return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates notification-based authentication session status + */ +public class NotificationAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validators based on certificate level + */ + public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + } + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @return a new instance of {@link NotificationAuthenticationResponseValidator} + */ + public static NotificationAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new NotificationAuthenticationResponseValidator(certificateValidator, + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name used in the QR-code or device link + * @return the authentication identity + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + *

+ * Should only be used for QR-code or notification-based authentication validation + * + * @param sessionStatus the authentication session status to be validated + * @param authenticationSessionRequest the authentication session request that was used to start the session + * @param schemaName the schema name used in the QR-code or device link + * @param brokeredRpName the brokered relying party name + * @return authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest.certificateLevel() == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), + authenticationResponse.getInteractionTypeUsed(), + "", + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index b694ffb5..faeb4a4a 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -1,315 +1,315 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a notification-based authentication session - */ -public class NotificationAuthenticationSessionRequestBuilder { - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel; - private String rpChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; - private List interactions; - private Boolean shareMdClientIpAddress; - private Set capabilities; - private SemanticsIdentifier semanticsIdentifier; - private String documentNumber; - - private NotificationAuthenticationSessionRequest notificationAuthenticationSessionRequest; - - /** - * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public NotificationAuthenticationSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartUUID the relying party UUID - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { - this.relyingPartyUUID = relyingPartUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the RP challenge - *

- * The provided rpChallenge must be a Base64 encoded string - *

- * Use {@link ee.sk.smartid.RpChallengeGenerator#generate()} to generate a valid RP challenge - * - * @param rpChallenge RP challenge in Base64 encoded format - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { - this.rpChallenge = rpChallenge; - return this; - } - - /** - * Sets the signature algorithm - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the hash algorithm - * - * @param hashAlgorithm the hash algorithm - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return this; - } - - /** - * Sets the interactions - * - * @param interactions the notification interactions - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the authentication session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the document number - *

- * Setting this value will make the authentication session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sends the authentication request and get the init session response - *

- * There are 2 supported ways to start authentication session: - *

    - *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
  • with document number by using {@link #withDocumentNumber(String)}
  • - *
- * - * @return init session response - */ - public NotificationAuthenticationSessionResponse initAuthenticationSession() { - validateRequestParameters(); - NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); - validateResponseParameters(notificationAuthenticationSessionResponse); - this.notificationAuthenticationSessionRequest = authenticationRequest; - return notificationAuthenticationSessionResponse; - } - - /** - * Returns the built authentication session request - * - * @return the built authentication session request - */ - public NotificationAuthenticationSessionRequest getAuthenticationSessionRequest() { - if (notificationAuthenticationSessionRequest == null) { - throw new SmartIdClientException("Notification-based authentication session has not been initialized yet"); - } - return notificationAuthenticationSessionRequest; - } - - private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } else if (semanticsIdentifier != null) { - return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initNotificationAuthentication(authenticationRequest, documentNumber); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); - } - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - validateSignatureParameters(); - validateInteractions(); - } - - private void validateSignatureParameters() { - if (StringUtil.isEmpty(rpChallenge)) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); - } - try { - Base64.getDecoder().decode(rpChallenge); - } catch (IllegalArgumentException e) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); - } - if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private NotificationAuthenticationSessionRequest createAuthenticationRequest() { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(hashAlgorithm.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2.name(), - signatureProtocolParameters, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - capabilities, - VerificationCodeType.NUMERIC4.getValue() - ); - } - - private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { - if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based authentication session initialisation response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a notification-based authentication session + */ +public class NotificationAuthenticationSessionRequestBuilder { + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel; + private String rpChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + + private NotificationAuthenticationSessionRequest notificationAuthenticationSessionRequest; + + /** + * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the RP challenge + *

+ * The provided rpChallenge must be a Base64 encoded string + *

+ * Use {@link ee.sk.smartid.RpChallengeGenerator#generate()} to generate a valid RP challenge + * + * @param rpChallenge RP challenge in Base64 encoded format + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the hash algorithm + * + * @param hashAlgorithm the hash algorithm + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the interactions + * + * @param interactions the notification interactions + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

+ * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

+ * There are 2 supported ways to start authentication session: + *

    + *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
  • with document number by using {@link #withDocumentNumber(String)}
  • + *
+ * + * @return init session response + */ + public NotificationAuthenticationSessionResponse initAuthenticationSession() { + validateRequestParameters(); + NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(notificationAuthenticationSessionResponse); + this.notificationAuthenticationSessionRequest = authenticationRequest; + return notificationAuthenticationSessionResponse; + } + + /** + * Returns the built authentication session request + * + * @return the built authentication session request + */ + public NotificationAuthenticationSessionRequest getAuthenticationSessionRequest() { + if (notificationAuthenticationSessionRequest == null) { + throw new SmartIdClientException("Notification-based authentication session has not been initialized yet"); + } + return notificationAuthenticationSessionRequest; + } + + private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } else if (semanticsIdentifier != null) { + return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initNotificationAuthentication(authenticationRequest, documentNumber); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + validateSignatureParameters(); + validateInteractions(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(rpChallenge)) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); + } + try { + Base64.getDecoder().decode(rpChallenge); + } catch (IllegalArgumentException e) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); + } + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private NotificationAuthenticationSessionRequest createAuthenticationRequest() { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(hashAlgorithm.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2.name(), + signatureProtocolParameters, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + VerificationCodeType.NUMERIC4.getValue() + ); + } + + private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { + if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based authentication session initialisation response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 09dab6f5..908a8d70 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -1,195 +1,195 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for notification-based certificate choice session requests - */ -public class NotificationCertificateChoiceSessionRequestBuilder { - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private Boolean shareMdClientIpAddress; - private SemanticsIdentifier semanticsIdentifier; - - /** - * Constructs a new NotificationCertificateChoiceSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public NotificationCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce - * - * @param nonce the nonce - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the notification session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Initializes a notification-based certificate choice session - * - * @return init session response - * @throws SmartIdRequestSetupException whe the provided request parameters are invalid - * @throws UnprocessableSmartIdResponseException when the response is missing required parameters - * @throws SmartIdClientException when the request could not be sent - */ - public NotificationCertificateChoiceSessionResponse initCertificateChoice() { - validateRequestParameters(); - NotificationCertificateChoiceSessionRequest request = createCertificateChoiceRequest(); - NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); - validateResponseParameters(notificationCertificateChoiceSessionResponse); - return notificationCertificateChoiceSessionResponse; - } - - private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(NotificationCertificateChoiceSessionRequest request) { - if (semanticsIdentifier == null) { - throw new SmartIdRequestSetupException("Value for 'semanticIdentifier' must be set"); - } - return connector.initNotificationCertificateChoice(request, semanticsIdentifier); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); - } - } - - private NotificationCertificateChoiceSessionRequest createCertificateChoiceRequest() { - return new NotificationCertificateChoiceSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - nonce, - capabilities, - shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null); - } - - private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for notification-based certificate choice session requests + */ +public class NotificationCertificateChoiceSessionRequestBuilder { + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private Boolean shareMdClientIpAddress; + private SemanticsIdentifier semanticsIdentifier; + + /** + * Constructs a new NotificationCertificateChoiceSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the notification session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Initializes a notification-based certificate choice session + * + * @return init session response + * @throws SmartIdRequestSetupException whe the provided request parameters are invalid + * @throws UnprocessableSmartIdResponseException when the response is missing required parameters + * @throws SmartIdClientException when the request could not be sent + */ + public NotificationCertificateChoiceSessionResponse initCertificateChoice() { + validateRequestParameters(); + NotificationCertificateChoiceSessionRequest request = createCertificateChoiceRequest(); + NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); + validateResponseParameters(notificationCertificateChoiceSessionResponse); + return notificationCertificateChoiceSessionResponse; + } + + private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(NotificationCertificateChoiceSessionRequest request) { + if (semanticsIdentifier == null) { + throw new SmartIdRequestSetupException("Value for 'semanticIdentifier' must be set"); + } + return connector.initNotificationCertificateChoice(request, semanticsIdentifier); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); + } + } + + private NotificationCertificateChoiceSessionRequest createCertificateChoiceRequest() { + return new NotificationCertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null); + } + + private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index ead4a53b..19295f5a 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -1,338 +1,338 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.VerificationCode; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a notification-based signature session - */ -public class NotificationSignatureSessionRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); - - private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[0-9]{4}$"); - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private SemanticsIdentifier semanticsIdentifier; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private List interactions; - private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private DigestInput digestInput; - - /** - * Constructs a new Smart-ID signature request builder with the given connector. - * - * @param connector the connector - */ - public NotificationSignatureSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the semantics identifier. - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the interactions. - * - * @param interactions the allowed interactions order - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the data to be signed. - *

- * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - *

- * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. - * - * @param signableData the data to be signed - * @return this builder instance - * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableHash} - */ - public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (this.digestInput != null && this.digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash"); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the hash to be signed in the signature protocol. - *

- * The provided {@link SignableHash} must contain a valid hash value and hash type, - * which will be used as the digest in the signing request. - *

- * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. - * - * @param signableHash the hash data to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableData} - */ - public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (this.digestInput != null && this.digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData"); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sends the signature request and initiates a notification-based signature session. - *

- * There are two supported ways to start the signature session: - *

    - *
  • with a document number by using {@link #withDocumentNumber(String)}
  • - *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
- * - * @return a {@link NotificationSignatureSessionResponse} containing session details such as session ID and verification code - * @throws SmartIdRequestSetupException when the request parameters are not set correctly - * @throws UnprocessableSmartIdResponseException when the response from the Smart-ID service is invalid - */ - public NotificationSignatureSessionResponse initSignatureSession() { - validateRequestParameters(); - NotificationSignatureSessionRequest request = createSignatureSessionRequest(); - NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(request); - validateResponseParameters(notificationSignatureSessionResponse); - return notificationSignatureSessionResponse; - } - - private NotificationSignatureSessionResponse initSignatureSession(NotificationSignatureSessionRequest request) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (documentNumber != null) { - return connector.initNotificationSignature(request, documentNumber); - } else if (semanticsIdentifier != null) { - return connector.initNotificationSignature(request, semanticsIdentifier); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); - } - } - - private NotificationSignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - - return new NotificationSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - nonce, - capabilities, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null - ); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); - } - validateInteractions(); - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateResponseParameters(NotificationSignatureSessionResponse response) { - if (StringUtil.isEmpty(response.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'sessionID' is missing or empty"); - } - - VerificationCode verificationCode = response.vc(); - if (verificationCode == null) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc' is missing"); - } - String vcType = verificationCode.type(); - if (StringUtil.isEmpty(vcType)) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' is missing or empty"); - } - if (!VerificationCodeType.NUMERIC4.getValue().equals(vcType)) { - logger.error("Notification-based signature response field 'vc.type' contains unsupported value '{}'", vcType); - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' contains unsupported value"); - } - if (StringUtil.isEmpty(verificationCode.value())) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' is missing or empty"); - } - if (!VERIFICATION_CODE_PATTERN.matcher(verificationCode.value()).matches()) { - logger.error("Notification-based signature response field 'vc.value' does not match the required pattern. Expected pattern: {}; actual value: {}", - VERIFICATION_CODE_PATTERN.pattern(), verificationCode.value()); - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' does not match the required pattern"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a notification-based signature session + */ +public class NotificationSignatureSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); + + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[0-9]{4}$"); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List interactions; + private Boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private DigestInput digestInput; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public NotificationSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the interactions. + * + * @param interactions the allowed interactions order + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

+ * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + *

+ * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. + * + * @param signableData the data to be signed + * @return this builder instance + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableHash} + */ + public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash"); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

+ * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + *

+ * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. + * + * @param signableHash the hash data to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableData} + */ + public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData"); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sends the signature request and initiates a notification-based signature session. + *

+ * There are two supported ways to start the signature session: + *

    + *
  • with a document number by using {@link #withDocumentNumber(String)}
  • + *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
+ * + * @return a {@link NotificationSignatureSessionResponse} containing session details such as session ID and verification code + * @throws SmartIdRequestSetupException when the request parameters are not set correctly + * @throws UnprocessableSmartIdResponseException when the response from the Smart-ID service is invalid + */ + public NotificationSignatureSessionResponse initSignatureSession() { + validateRequestParameters(); + NotificationSignatureSessionRequest request = createSignatureSessionRequest(); + NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(request); + validateResponseParameters(notificationSignatureSessionResponse); + return notificationSignatureSessionResponse; + } + + private NotificationSignatureSessionResponse initSignatureSession(NotificationSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (documentNumber != null) { + return connector.initNotificationSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initNotificationSignature(request, semanticsIdentifier); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); + } + } + + private NotificationSignatureSessionRequest createSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + + return new NotificationSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce, + capabilities, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null + ); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } + validateInteractions(); + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateResponseParameters(NotificationSignatureSessionResponse response) { + if (StringUtil.isEmpty(response.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'sessionID' is missing or empty"); + } + + VerificationCode verificationCode = response.vc(); + if (verificationCode == null) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc' is missing"); + } + String vcType = verificationCode.type(); + if (StringUtil.isEmpty(vcType)) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' is missing or empty"); + } + if (!VerificationCodeType.NUMERIC4.getValue().equals(vcType)) { + logger.error("Notification-based signature response field 'vc.type' contains unsupported value '{}'", vcType); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' contains unsupported value"); + } + if (StringUtil.isEmpty(verificationCode.value())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' is missing or empty"); + } + if (!VERIFICATION_CODE_PATTERN.matcher(verificationCode.value()).matches()) { + logger.error("Notification-based signature response field 'vc.value' does not match the required pattern. Expected pattern: {}; actual value: {}", + VERIFICATION_CODE_PATTERN.pattern(), verificationCode.value()); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' does not match the required pattern"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/QrCodeGenerator.java b/src/main/java/ee/sk/smartid/QrCodeGenerator.java index 4daed4e7..7e3a3844 100644 --- a/src/main/java/ee/sk/smartid/QrCodeGenerator.java +++ b/src/main/java/ee/sk/smartid/QrCodeGenerator.java @@ -1,142 +1,142 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; -import static com.google.zxing.EncodeHintType.MARGIN; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; - -import javax.imageio.ImageIO; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * This class is responsible for generating QR-codes. - * It can generate QR-codes as Data URIs or as BufferedImages. - *

- * The default image size of the generated QR code is 610x610px. - * It is calculated based on the version 9 QR-code that contains 53x53 modules and four quiet area modules. - * QR-code version 9 is selected automatically based on the provided length of the data. - * The module size should be 10px, so the image size is 53x10=530px + 2x4x10=80px = 610px. - *

- * Generated QR-codes have LOW error correction level. - */ -public class QrCodeGenerator { - - private static final int DEFAULT_QR_CODE_WIDTH_PX = 610; - private static final int DEFAULT_QR_CODE_HEIGHT = 610; - private static final int DEFAULT_QUIET_AREA_SIZE_MODULES = 4; - private static final String DEFAULT_FILE_FORMAT = "png"; - - /** - * Generates a QR-code as Data URI - *

- * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). - * - * @param data the data to be encoded - * @return the QR-code as a Base64 encoded string - */ - public static String generateDataUri(String data) { - BufferedImage bufferedImage = generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); - return convertToDataUri(bufferedImage, DEFAULT_FILE_FORMAT); - } - - /** - * Generates a QR-code as BufferedImage - *

- * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). - * - * @param data the data to be encoded - * @return the QR-code as a BufferedImage - */ - public static BufferedImage generateImage(String data) { - return generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); - } - - /** - * Generates a QR-code as BufferedImage. - *

- * Provide the width and height of the image in pixels and the size of the quiet area around the QR-code in modules. - * - * @param data the data to be encoded - * @param widthPx the width of the image in pixels - * @param heightPx the height of the image in pixels - * @param quietAreaSize the size of the quiet area around the QR-code, value in modules - * @return the QR-code as a BufferedImage - */ - public static BufferedImage generateImage(String data, int widthPx, int heightPx, int quietAreaSize) { - if (data == null || data.isEmpty()) { - throw new SmartIdClientException("Provided data cannot be empty"); - } - BitMatrix matrix; - try { - Map hints = new HashMap<>(); - hints.put(MARGIN, quietAreaSize); - hints.put(ERROR_CORRECTION, ErrorCorrectionLevel.L); - - matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, widthPx, heightPx, hints); - } catch (WriterException ex) { - throw new SmartIdClientException("Unable to create QR-code", ex); - } - return MatrixToImageWriter.toBufferedImage(matrix); - } - - /** - * Converts provided BufferedImage to Data URI with provided file format. - * - * @param bufferedImage the image to be converted - * @param fileFormat the format of the image - * @return the image as a Data URI - */ - public static String convertToDataUri(BufferedImage bufferedImage, String fileFormat) { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(bufferedImage, fileFormat, outputStream); - String imgBase64 = Base64.getMimeEncoder().encodeToString(outputStream.toByteArray()); - return toDataUri(imgBase64, fileFormat); - } catch (IOException ex) { - throw new SmartIdClientException("Unable to generate QR-code", ex); - } - } - - private static String toDataUri(String imageData, String fileFormat) { - return String.format("data:image/%s;base64,%s", fileFormat, imageData); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; +import static com.google.zxing.EncodeHintType.MARGIN; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * This class is responsible for generating QR-codes. + * It can generate QR-codes as Data URIs or as BufferedImages. + *

+ * The default image size of the generated QR code is 610x610px. + * It is calculated based on the version 9 QR-code that contains 53x53 modules and four quiet area modules. + * QR-code version 9 is selected automatically based on the provided length of the data. + * The module size should be 10px, so the image size is 53x10=530px + 2x4x10=80px = 610px. + *

+ * Generated QR-codes have LOW error correction level. + */ +public class QrCodeGenerator { + + private static final int DEFAULT_QR_CODE_WIDTH_PX = 610; + private static final int DEFAULT_QR_CODE_HEIGHT = 610; + private static final int DEFAULT_QUIET_AREA_SIZE_MODULES = 4; + private static final String DEFAULT_FILE_FORMAT = "png"; + + /** + * Generates a QR-code as Data URI + *

+ * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a Base64 encoded string + */ + public static String generateDataUri(String data) { + BufferedImage bufferedImage = generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + return convertToDataUri(bufferedImage, DEFAULT_FILE_FORMAT); + } + + /** + * Generates a QR-code as BufferedImage + *

+ * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data) { + return generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + } + + /** + * Generates a QR-code as BufferedImage. + *

+ * Provide the width and height of the image in pixels and the size of the quiet area around the QR-code in modules. + * + * @param data the data to be encoded + * @param widthPx the width of the image in pixels + * @param heightPx the height of the image in pixels + * @param quietAreaSize the size of the quiet area around the QR-code, value in modules + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data, int widthPx, int heightPx, int quietAreaSize) { + if (data == null || data.isEmpty()) { + throw new SmartIdClientException("Provided data cannot be empty"); + } + BitMatrix matrix; + try { + Map hints = new HashMap<>(); + hints.put(MARGIN, quietAreaSize); + hints.put(ERROR_CORRECTION, ErrorCorrectionLevel.L); + + matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, widthPx, heightPx, hints); + } catch (WriterException ex) { + throw new SmartIdClientException("Unable to create QR-code", ex); + } + return MatrixToImageWriter.toBufferedImage(matrix); + } + + /** + * Converts provided BufferedImage to Data URI with provided file format. + * + * @param bufferedImage the image to be converted + * @param fileFormat the format of the image + * @return the image as a Data URI + */ + public static String convertToDataUri(BufferedImage bufferedImage, String fileFormat) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, fileFormat, outputStream); + String imgBase64 = Base64.getMimeEncoder().encodeToString(outputStream.toByteArray()); + return toDataUri(imgBase64, fileFormat); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to generate QR-code", ex); + } + } + + private static String toDataUri(String imageData, String fileFormat) { + return String.format("data:image/%s;base64,%s", fileFormat, imageData); + } +} diff --git a/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java index f0489e4b..48bbd710 100644 --- a/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java @@ -1,128 +1,128 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the signature certificate is a qualified Smart-ID certificate and can be used for digital signing. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Qualified profile - *

- * Additionally, it will check that certificate can be used for qualified electronic signature by checking - * presence of QCStatements extension and that it contains the electronic signature OID. - */ -public class QualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { - - private static final Logger logger = LoggerFactory.getLogger(QualifiedSignatureCertificatePurposeValidator.class); - - private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); - - @Override - public void validate(X509Certificate certificate) { - validateCertificateHasQualifiedSmartIdCertificatePolicies(certificate); - validateCertificateCanBeUsedForSigning(certificate); - validateCertificateCanBeUsedForQualifiedElectronicSignature(certificate); - } - - private static void validateCertificateHasQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); - } - if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate does not contain required qualified certificate policy OIDs"); - } - } - - private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { - if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); - } - } - - private static void validateCertificateCanBeUsedForQualifiedElectronicSignature(X509Certificate certificate) { - byte[] extensionValue = certificate.getExtensionValue(Extension.qCStatements.getId()); - if (extensionValue == null) { - throw new UnprocessableSmartIdResponseException("Certificate does not have 'QCStatements' extension"); - } - if (!hasElectronicSigningOid(extensionValue)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have electronic signature OID (" + ETSIQCObjectIdentifiers.id_etsi_qct_esign.getId() + ") in QCStatements extension."); - } - } - - private static boolean hasElectronicSigningOid(byte[] extensionValue) { - ASN1Primitive prim; - try { - prim = ASN1Primitive.fromByteArray(ASN1OctetString.getInstance(extensionValue).getOctets()); - } catch (IOException ex) { - throw new SmartIdClientException("Unable to parse QCStatements extension", ex); - } - - ASN1Sequence qcStatements = ASN1Sequence.getInstance(prim); - for (int i = 0; i < qcStatements.size(); i++) { - QCStatement qs = QCStatement.getInstance(qcStatements.getObjectAt(i)); - ASN1ObjectIdentifier stmtId = qs.getStatementId(); - - if (ETSIQCObjectIdentifiers.id_etsi_qcs_QcType.equals(stmtId)) { - ASN1Sequence typeSeq = ASN1Sequence.getInstance(qs.getStatementInfo()); - if (typeSeq == null) { - return false; - } - for (int j = 0; j < typeSeq.size(); j++) { - ASN1ObjectIdentifier typeOid = ASN1ObjectIdentifier.getInstance(typeSeq.getObjectAt(j)); - if (ETSIQCObjectIdentifiers.id_etsi_qct_esign.equals(typeOid)) { - return true; - } - } - } - } - return false; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a qualified Smart-ID certificate and can be used for digital signing. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Qualified profile + *

+ * Additionally, it will check that certificate can be used for qualified electronic signature by checking + * presence of QCStatements extension and that it contains the electronic signature OID. + */ +public class QualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + private static final Logger logger = LoggerFactory.getLogger(QualifiedSignatureCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); + + @Override + public void validate(X509Certificate certificate) { + validateCertificateHasQualifiedSmartIdCertificatePolicies(certificate); + validateCertificateCanBeUsedForSigning(certificate); + validateCertificateCanBeUsedForQualifiedElectronicSignature(certificate); + } + + private static void validateCertificateHasQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate does not contain required qualified certificate policy OIDs"); + } + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } + + private static void validateCertificateCanBeUsedForQualifiedElectronicSignature(X509Certificate certificate) { + byte[] extensionValue = certificate.getExtensionValue(Extension.qCStatements.getId()); + if (extensionValue == null) { + throw new UnprocessableSmartIdResponseException("Certificate does not have 'QCStatements' extension"); + } + if (!hasElectronicSigningOid(extensionValue)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have electronic signature OID (" + ETSIQCObjectIdentifiers.id_etsi_qct_esign.getId() + ") in QCStatements extension."); + } + } + + private static boolean hasElectronicSigningOid(byte[] extensionValue) { + ASN1Primitive prim; + try { + prim = ASN1Primitive.fromByteArray(ASN1OctetString.getInstance(extensionValue).getOctets()); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse QCStatements extension", ex); + } + + ASN1Sequence qcStatements = ASN1Sequence.getInstance(prim); + for (int i = 0; i < qcStatements.size(); i++) { + QCStatement qs = QCStatement.getInstance(qcStatements.getObjectAt(i)); + ASN1ObjectIdentifier stmtId = qs.getStatementId(); + + if (ETSIQCObjectIdentifiers.id_etsi_qcs_QcType.equals(stmtId)) { + ASN1Sequence typeSeq = ASN1Sequence.getInstance(qs.getStatementInfo()); + if (typeSeq == null) { + return false; + } + for (int j = 0; j < typeSeq.size(); j++) { + ASN1ObjectIdentifier typeOid = ASN1ObjectIdentifier.getInstance(typeSeq.getObjectAt(j)); + if (ETSIQCObjectIdentifiers.id_etsi_qct_esign.equals(typeOid)) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/ee/sk/smartid/RpChallenge.java b/src/main/java/ee/sk/smartid/RpChallenge.java index eefc7ac7..0063dfa3 100644 --- a/src/main/java/ee/sk/smartid/RpChallenge.java +++ b/src/main/java/ee/sk/smartid/RpChallenge.java @@ -1,55 +1,55 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import org.bouncycastle.util.encoders.Base64; - -/** - * Represents an RP challenge - * - * @param value a byte array of representing the challenge - */ -public record RpChallenge(byte[] value) { - - /** - * Returns a copy of the challenge value - * - * @return a byte array representing the challenge - */ - public byte[] value() { - return value.clone(); - } - - /** - * Returns the Base64 encoded representation of the challenge value - * - * @return a Base64 encoded string representing the challenge - */ - public String toBase64EncodedValue() { - return Base64.toBase64String(value); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import org.bouncycastle.util.encoders.Base64; + +/** + * Represents an RP challenge + * + * @param value a byte array of representing the challenge + */ +public record RpChallenge(byte[] value) { + + /** + * Returns a copy of the challenge value + * + * @return a byte array representing the challenge + */ + public byte[] value() { + return value.clone(); + } + + /** + * Returns the Base64 encoded representation of the challenge value + * + * @return a Base64 encoded string representing the challenge + */ + public String toBase64EncodedValue() { + return Base64.toBase64String(value); + } +} diff --git a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java index d55d62d9..a2a97f68 100644 --- a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java +++ b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java @@ -1,74 +1,74 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.SecureRandom; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for generating RP challenge - */ -public class RpChallengeGenerator { - - private static final int MAX_LENGTH = 64; - private static final int MIN_LENGTH = 32; - - private RpChallengeGenerator() { - } - - /** - * Generates an RP challenge with a maximum length of 64 bytes - * - * @return RP challenge - */ - public static RpChallenge generate() { - byte[] randBytes = new byte[MAX_LENGTH]; - new SecureRandom().nextBytes(randBytes); - return new RpChallenge(randBytes); - } - - /** - * Generates an RP challenge with specified length - * - * @param length length of the challenge - * @return RP challenge - */ - public static RpChallenge generate(int length) { - if (length < MIN_LENGTH || length > MAX_LENGTH) { - throw new SmartIdClientException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); - } - byte[] randBytes = getRandomBytes(length); - return new RpChallenge(randBytes); - } - - private static byte[] getRandomBytes(int length) { - byte[] randBytes = new byte[length]; - new SecureRandom().nextBytes(randBytes); - return randBytes; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for generating RP challenge + */ +public class RpChallengeGenerator { + + private static final int MAX_LENGTH = 64; + private static final int MIN_LENGTH = 32; + + private RpChallengeGenerator() { + } + + /** + * Generates an RP challenge with a maximum length of 64 bytes + * + * @return RP challenge + */ + public static RpChallenge generate() { + byte[] randBytes = new byte[MAX_LENGTH]; + new SecureRandom().nextBytes(randBytes); + return new RpChallenge(randBytes); + } + + /** + * Generates an RP challenge with specified length + * + * @param length length of the challenge + * @return RP challenge + */ + public static RpChallenge generate(int length) { + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new SmartIdClientException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); + } + byte[] randBytes = getRandomBytes(length); + return new RpChallenge(randBytes); + } + + private static byte[] getRandomBytes(int length) { + byte[] randBytes = new byte[length]; + new SecureRandom().nextBytes(randBytes); + return randBytes; + } +} diff --git a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java index 81bc797f..db7c0671 100644 --- a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java +++ b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java @@ -1,140 +1,140 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Encapsulates multiple parameters of RSASSA-PSS - */ -public class RsaSsaPssParameters { - - private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - - private HashAlgorithm digestHashAlgorithm; - private MaskGenAlgorithm maskGenAlgorithm; - private HashAlgorithm maskHashAlgorithm; - private int saltLength; - private TrailerField trailerField; - - /** - * Sets the hash algorithm - * - * @param digestHashAlgorithm the hash algorithm; see {@link HashAlgorithm} - */ - public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { - this.digestHashAlgorithm = digestHashAlgorithm; - } - - /** - * Sets the mask generation algorithm - * - * @param maskGenAlgorithm the mask generation algorithm; see {@link MaskGenAlgorithm} - */ - public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - /** - * Sets the mask hash algorithm - * - * @param maskHashAlgorithm the mask hash algorithm; see {@link HashAlgorithm} - */ - public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { - this.maskHashAlgorithm = maskHashAlgorithm; - } - - /** - * Sets the salt length - * - * @param saltLength the salt length in bytes - */ - public void setSaltLength(int saltLength) { - this.saltLength = saltLength; - } - - /** - * Sets the trailer field - * - * @param trailerField the trailer field; see {@link TrailerField} - */ - public void setTrailerField(TrailerField trailerField) { - this.trailerField = trailerField; - } - - /** - * Gets the signature algorithm - * - * @return the signature algorithm; see {@link SignatureAlgorithm} - */ - public SignatureAlgorithm getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Gets the hash algorithm - * - * @return the hash algorithm; see {@link HashAlgorithm} - */ - public HashAlgorithm getDigestHashAlgorithm() { - return digestHashAlgorithm; - } - - /** - * Gets the mask generation algorithm - * - * @return the mask generation algorithm - */ - public MaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - /** - * Gets the mask hash algorithm - * - * @return the mask hash algorithm - */ - public HashAlgorithm getMaskHashAlgorithm() { - return maskHashAlgorithm; - } - - /** - * Gets the salt length - * - * @return the salt length in bytes - */ - public int getSaltLength() { - return saltLength; - } - - /** - * Gets the trailer field - * - * @return the trailer field - */ - public TrailerField getTrailerField() { - return trailerField; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Encapsulates multiple parameters of RSASSA-PSS + */ +public class RsaSsaPssParameters { + + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + + private HashAlgorithm digestHashAlgorithm; + private MaskGenAlgorithm maskGenAlgorithm; + private HashAlgorithm maskHashAlgorithm; + private int saltLength; + private TrailerField trailerField; + + /** + * Sets the hash algorithm + * + * @param digestHashAlgorithm the hash algorithm; see {@link HashAlgorithm} + */ + public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { + this.digestHashAlgorithm = digestHashAlgorithm; + } + + /** + * Sets the mask generation algorithm + * + * @param maskGenAlgorithm the mask generation algorithm; see {@link MaskGenAlgorithm} + */ + public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + /** + * Sets the mask hash algorithm + * + * @param maskHashAlgorithm the mask hash algorithm; see {@link HashAlgorithm} + */ + public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { + this.maskHashAlgorithm = maskHashAlgorithm; + } + + /** + * Sets the salt length + * + * @param saltLength the salt length in bytes + */ + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + /** + * Sets the trailer field + * + * @param trailerField the trailer field; see {@link TrailerField} + */ + public void setTrailerField(TrailerField trailerField) { + this.trailerField = trailerField; + } + + /** + * Gets the signature algorithm + * + * @return the signature algorithm; see {@link SignatureAlgorithm} + */ + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Gets the hash algorithm + * + * @return the hash algorithm; see {@link HashAlgorithm} + */ + public HashAlgorithm getDigestHashAlgorithm() { + return digestHashAlgorithm; + } + + /** + * Gets the mask generation algorithm + * + * @return the mask generation algorithm + */ + public MaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + /** + * Gets the mask hash algorithm + * + * @return the mask hash algorithm + */ + public HashAlgorithm getMaskHashAlgorithm() { + return maskHashAlgorithm; + } + + /** + * Gets the salt length + * + * @return the salt length in bytes + */ + public int getSaltLength() { + return saltLength; + } + + /** + * Gets the trailer field + * + * @return the trailer field + */ + public TrailerField getTrailerField() { + return trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/SessionType.java b/src/main/java/ee/sk/smartid/SessionType.java index be0b8b0f..7a8c58f7 100644 --- a/src/main/java/ee/sk/smartid/SessionType.java +++ b/src/main/java/ee/sk/smartid/SessionType.java @@ -1,61 +1,61 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Represents session types used to construct different device links for Smart-ID sessions. - */ -public enum SessionType { - - /** - * Authentication session type - */ - AUTHENTICATION("auth"), - /** - * Signature session type - */ - SIGNATURE("sign"), - /** - * Certificate choice session type - */ - CERTIFICATE_CHOICE("cert"); - - private final String value; - - SessionType(String value) { - this.value = value; - } - - /** - * Returns the value used in the device link for the session type. - * - * @return the string value of the session type. - */ - public String getValue() { - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Represents session types used to construct different device links for Smart-ID sessions. + */ +public enum SessionType { + + /** + * Authentication session type + */ + AUTHENTICATION("auth"), + /** + * Signature session type + */ + SIGNATURE("sign"), + /** + * Certificate choice session type + */ + CERTIFICATE_CHOICE("cert"); + + private final String value; + + SessionType(String value) { + this.value = value; + } + + /** + * Returns the value used in the device link for the session type. + * + * @return the string value of the session type. + */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java index 29a7495a..b40af066 100644 --- a/src/main/java/ee/sk/smartid/SignableData.java +++ b/src/main/java/ee/sk/smartid/SignableData.java @@ -1,93 +1,93 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * This class can be used to contain the data - * to be signed when it is not yet in hashed format - *

- * {@link SignableHash} can be used - * instead when the data to be signed is already - * in hashed format. - */ -public record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - - /** - * Creates a new instance of SignableData - *

- * Will use SHA-512 as the default hashing algorithm - * - * @param dataToSign byte array of data to be signed - */ - public SignableData(byte[] dataToSign) { - this(dataToSign, HashAlgorithm.SHA_512); - } - - /** - * Creates a new instance of SignableData - * - * @param dataToSign byte array of data to be signed - * @param hashAlgorithm hashing algorithm to be used - * @throws SmartIdRequestSetupException when input values are missing or empty - */ - public SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) { - if (dataToSign == null || dataToSign.length == 0) { - throw new SmartIdRequestSetupException("Parameter 'dataToSign' cannot be empty"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); - } - this.dataToSign = dataToSign.clone(); - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Calculates the digest of the data to be signed - * and returns it in Base64 encoded format - * - * @return Base64 encoded hash - */ - @Override - public String getDigestInBase64() { - byte[] digest = calculateHash(); - return Base64.getEncoder().encodeToString(digest); - } - - /** - * Calculates the digest of the data to be signed - * - * @return hash - */ - public byte[] calculateHash() { - return DigestCalculator.calculateDigest(dataToSign, hashAlgorithm); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * This class can be used to contain the data + * to be signed when it is not yet in hashed format + *

+ * {@link SignableHash} can be used + * instead when the data to be signed is already + * in hashed format. + */ +public record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { + + /** + * Creates a new instance of SignableData + *

+ * Will use SHA-512 as the default hashing algorithm + * + * @param dataToSign byte array of data to be signed + */ + public SignableData(byte[] dataToSign) { + this(dataToSign, HashAlgorithm.SHA_512); + } + + /** + * Creates a new instance of SignableData + * + * @param dataToSign byte array of data to be signed + * @param hashAlgorithm hashing algorithm to be used + * @throws SmartIdRequestSetupException when input values are missing or empty + */ + public SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) { + if (dataToSign == null || dataToSign.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'dataToSign' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } + this.dataToSign = dataToSign.clone(); + this.hashAlgorithm = hashAlgorithm; + } + + /** + * Calculates the digest of the data to be signed + * and returns it in Base64 encoded format + * + * @return Base64 encoded hash + */ + @Override + public String getDigestInBase64() { + byte[] digest = calculateHash(); + return Base64.getEncoder().encodeToString(digest); + } + + /** + * Calculates the digest of the data to be signed + * + * @return hash + */ + public byte[] calculateHash() { + return DigestCalculator.calculateDigest(dataToSign, hashAlgorithm); + } +} diff --git a/src/main/java/ee/sk/smartid/SignableHash.java b/src/main/java/ee/sk/smartid/SignableHash.java index 9d18117a..8bc47191 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/SignableHash.java @@ -1,87 +1,87 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * This class can be used to contain the hash - * to be signed - *

- * {@link SignableData} can be used - * instead when the data to be signed is not already - * in hashed format. - */ -public record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - - /** - * Creates {@link SignableHash} instance, - *

- * Will use SHA-512 as the default hashing algorithm - * - * @param hashToSign byte array of hash to be signed - * @throws SmartIdRequestSetupException when hashToSign is missing or empty - */ - public SignableHash(byte[] hashToSign) { - this(hashToSign, HashAlgorithm.SHA_512); - } - - /** - * Creates {@link SignableHash} instance - * - * @param hashToBeSigned byte array of hash to be signed - * @param hashAlgorithm hashing algorithm used to create the hash - * @throws SmartIdRequestSetupException when input parameters are missing or empty - */ - public SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) { - validateInputs(hashToBeSigned, hashAlgorithm); - this.hashToBeSigned = hashToBeSigned.clone(); - this.hashAlgorithm = hashAlgorithm; - } - - private static void validateInputs(byte[] hash, HashAlgorithm hashAlgorithm) { - if (hash == null || hash.length == 0) { - throw new SmartIdRequestSetupException("Parameter 'hash' cannot be empty"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); - } - } - - /** - * Get the hash as Base64-encoded string - * - * @return String - */ - @Override - public String getDigestInBase64() { - return Base64.getEncoder().encodeToString(hashToBeSigned); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * This class can be used to contain the hash + * to be signed + *

+ * {@link SignableData} can be used + * instead when the data to be signed is not already + * in hashed format. + */ +public record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { + + /** + * Creates {@link SignableHash} instance, + *

+ * Will use SHA-512 as the default hashing algorithm + * + * @param hashToSign byte array of hash to be signed + * @throws SmartIdRequestSetupException when hashToSign is missing or empty + */ + public SignableHash(byte[] hashToSign) { + this(hashToSign, HashAlgorithm.SHA_512); + } + + /** + * Creates {@link SignableHash} instance + * + * @param hashToBeSigned byte array of hash to be signed + * @param hashAlgorithm hashing algorithm used to create the hash + * @throws SmartIdRequestSetupException when input parameters are missing or empty + */ + public SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) { + validateInputs(hashToBeSigned, hashAlgorithm); + this.hashToBeSigned = hashToBeSigned.clone(); + this.hashAlgorithm = hashAlgorithm; + } + + private static void validateInputs(byte[] hash, HashAlgorithm hashAlgorithm) { + if (hash == null || hash.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'hash' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } + } + + /** + * Get the hash as Base64-encoded string + * + * @return String + */ + @Override + public String getDigestInBase64() { + return Base64.getEncoder().encodeToString(hashToBeSigned); + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index f6a33872..2ae5afb9 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -1,82 +1,82 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Signature algorithms supported by Smart-ID API. - */ -public enum SignatureAlgorithm { - - /** - * RSASSA-PSS (RSA Probabilistic Signature Scheme) as defined in PKCS #1 v2.1. - * This algorithm provides probabilistic signature generation for enhanced security. - */ - RSASSA_PSS("rsassa-pss"); - - private final String algorithmName; - - SignatureAlgorithm(String algorithmName) { - this.algorithmName = algorithmName; - } - - /** - * Provides the signature algorithm name as used in the Smart-ID API. - * - * @return the signature algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Checks if the provided signature algorithm is supported. - * - * @param signatureAlgorithm the signature algorithm name to check - * @return true if the signature algorithm is supported, false otherwise - */ - public static boolean isSupported(String signatureAlgorithm) { - return Arrays.stream(SignatureAlgorithm.values()) - .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); - } - - /** - * Converts a string representation of a signature algorithm to its corresponding enum value. - * - * @param signatureAlgorithm the signature algorithm name - * @return the corresponding SignatureAlgorithm enum value - * @throws IllegalArgumentException if the provided signature algorithm is not supported - */ - public static SignatureAlgorithm fromString(String signatureAlgorithm) { - return Arrays - .stream(SignatureAlgorithm.values()) - .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid signatureAlgorithm value: " + signatureAlgorithm)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Signature algorithms supported by Smart-ID API. + */ +public enum SignatureAlgorithm { + + /** + * RSASSA-PSS (RSA Probabilistic Signature Scheme) as defined in PKCS #1 v2.1. + * This algorithm provides probabilistic signature generation for enhanced security. + */ + RSASSA_PSS("rsassa-pss"); + + private final String algorithmName; + + SignatureAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + /** + * Provides the signature algorithm name as used in the Smart-ID API. + * + * @return the signature algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Checks if the provided signature algorithm is supported. + * + * @param signatureAlgorithm the signature algorithm name to check + * @return true if the signature algorithm is supported, false otherwise + */ + public static boolean isSupported(String signatureAlgorithm) { + return Arrays.stream(SignatureAlgorithm.values()) + .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); + } + + /** + * Converts a string representation of a signature algorithm to its corresponding enum value. + * + * @param signatureAlgorithm the signature algorithm name + * @return the corresponding SignatureAlgorithm enum value + * @throws IllegalArgumentException if the provided signature algorithm is not supported + */ + public static SignatureAlgorithm fromString(String signatureAlgorithm) { + return Arrays + .stream(SignatureAlgorithm.values()) + .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid signatureAlgorithm value: " + signatureAlgorithm)); + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java index 3c91ed2d..7c2dbb29 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java @@ -1,46 +1,46 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating whether a given X509 certificate is suitable for digital signing purposes. - * Implementations should check certificate properties and throw an exception if the certificate is not valid for signing. - */ -public interface SignatureCertificatePurposeValidator { - - /** - * Validates that the provided certificate is suitable for digital signing - * - * @param certificate certificate to validate - * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for digital signing - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating whether a given X509 certificate is suitable for digital signing purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for signing. + */ +public interface SignatureCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for digital signing + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for digital signing + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java index 972c4704..c623f9aa 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java @@ -1,41 +1,41 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Factory interface to create instances of SignatureCertificatePurposeValidator based on the certificate level. - */ -public interface SignatureCertificatePurposeValidatorFactory { - - /** - * Creates SignatureCertificatePurposeValidator based on the provided certificate level. - * - * @param certificateLevel the certificate level to create the validator for - * @return SignatureCertificatePurposeValidator instance - */ - SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Factory interface to create instances of SignatureCertificatePurposeValidator based on the certificate level. + */ +public interface SignatureCertificatePurposeValidatorFactory { + + /** + * Creates SignatureCertificatePurposeValidator based on the provided certificate level. + * + * @param certificateLevel the certificate level to create the validator for + * @return SignatureCertificatePurposeValidator instance + */ + SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java index 1f07b2c3..1c18bd8a 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Factory to create Qualified or Non-Qualified SignatureCertificatePurposeValidator based on the certificate level. - * Will be used to validate the certificate purpose of the signature certificate. - *

- * Only QUALIFIED and ADVANCED certificate levels are supported, - * because QUALIFIED level certificate will also be returned for QSCD. - */ -public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { - - @Override - public SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel) { - if (certificateLevel == null) { - throw new SmartIdClientException("Parameter 'certificateLevel' is not provided"); - } - return switch (certificateLevel) { - case QUALIFIED -> new QualifiedSignatureCertificatePurposeValidator(); - case ADVANCED -> new NonQualifiedSignatureCertificatePurposeValidator(); - default -> throw new SmartIdClientException("Unsupported certificate level: " + certificateLevel); - }; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Factory to create Qualified or Non-Qualified SignatureCertificatePurposeValidator based on the certificate level. + * Will be used to validate the certificate purpose of the signature certificate. + *

+ * Only QUALIFIED and ADVANCED certificate levels are supported, + * because QUALIFIED level certificate will also be returned for QSCD. + */ +public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { + + @Override + public SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel) { + if (certificateLevel == null) { + throw new SmartIdClientException("Parameter 'certificateLevel' is not provided"); + } + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedSignatureCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedSignatureCertificatePurposeValidator(); + default -> throw new SmartIdClientException("Unsupported certificate level: " + certificateLevel); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureProtocol.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java index 4ee32e67..d30f0db1 100644 --- a/src/main/java/ee/sk/smartid/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -1,43 +1,43 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Signature protocols supported by Smart-ID API. - */ -public enum SignatureProtocol { - - /** - * Signature protocol used for authentication. - */ - ACSP_V2, - - /** - * Signature protocol used for signature. - */ - RAW_DIGEST_SIGNATURE -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Signature protocols supported by Smart-ID API. + */ +public enum SignatureProtocol { + + /** + * Signature protocol used for authentication. + */ + ACSP_V2, + + /** + * Signature protocol used for signature. + */ + RAW_DIGEST_SIGNATURE +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index 31e933d5..caad1cad 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -1,273 +1,273 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.security.cert.X509Certificate; -import java.util.Base64; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Response of a completed and validated signature session. - */ -public class SignatureResponse implements Serializable { - - private String endResult; - private String signatureValueInBase64; - private String algorithmName; - private SignatureAlgorithm signatureAlgorithm; - private FlowType flowType; - private X509Certificate certificate; - private CertificateLevel requestedCertificateLevel; - private CertificateLevel certificateLevel; - private String documentNumber; - private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name 'interactionTypeUsed'; Fix in SLIB-138 - private String deviceIpAddress; - private RsaSsaPssParameters rsaSsaPssParameters; - - /** - * Gets the signature value as a byte array by decoding the base64-encoded string. - * - * @return the signature value as a byte array - * @throws UnprocessableSmartIdResponseException if the base64 string is incorrectly encoded - */ - public byte[] getSignatureValue() { - try { - return Base64.getDecoder().decode(signatureValueInBase64); - } catch (IllegalArgumentException e) { - throw new UnprocessableSmartIdResponseException( - "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); - } - } - - /** - * Gets the end result of the signing operation. - *

- * returns the end result of the signing operation - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the signing operation. - * - * @param endResult the end result of the signing operation - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the signature value as a base64-encoded string. - * - * @return the signature value in base64 - */ - public String getSignatureValueInBase64() { - return signatureValueInBase64; - } - - /** - * Sets the signature value as a base64-encoded string. - * - * @param signatureValueInBase64 the signature value in base64 - */ - public void setSignatureValueInBase64(String signatureValueInBase64) { - this.signatureValueInBase64 = signatureValueInBase64; - } - - /** - * Gets the name of the algorithm used for signing. - * - * @return the name of the algorithm - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Sets the name of the algorithm used for signing. - * - * @param algorithmName the name of the algorithm - */ - public void setAlgorithmName(String algorithmName) { - this.algorithmName = algorithmName; - } - - /** - * Gets the signature algorithm used for signing. - * - * @return the signature algorithm - */ - public SignatureAlgorithm getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Sets the signature algorithm used for signing. - * - * @param signatureAlgorithm the signature algorithm - */ - public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - /** - * Gets the flow type user used to complete the signing. - * - * @return the flow type - */ - public FlowType getFlowType() { - return flowType; - } - - /** - * Sets the flow type. - * - * @param flowType the flow type - */ - public void setFlowType(FlowType flowType) { - this.flowType = flowType; - } - - /** - * Gets the certificate used for signing. - * - * @return the X.509 certificate - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate used for signing. - * - * @param certificate the X.509 certificate - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the certificate level of the certificate used for signing. - * - * @return the certificate level - */ - public CertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the certificate level of the certificate used for signing. - * - * @param certificateLevel the certificate level - */ - public void setCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the requested certificate level for the signing operation. - * - * @return the requested certificate level - */ - public CertificateLevel getRequestedCertificateLevel() { - return requestedCertificateLevel; - } - - /** - * Sets the requested certificate level for the signing operation. - * - * @param requestedCertificateLevel the requested certificate level - */ - public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { - this.requestedCertificateLevel = requestedCertificateLevel; - } - - /** - * Gets the document number of the user who performed the signing. - * - * @return the document number - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number of the user who performed the signing. - * - * @param documentNumber the document number - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * Gets the IP address of the device used by the user to complete the signing. - * - * @return the device IP address - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device. - * - * @param deviceIpAddress the device IP address - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - - /** - * Gets the RSASSA-PSS parameters used in the signing operation. - * - * @return the RSASSA-PSS parameters. - */ - public RsaSsaPssParameters getRsaSsaPssParameters() { - return rsaSsaPssParameters; - } - - /** - * Sets the RSASSA-PSS parameters used in the signing operation. - * - * @param rsaSsaPssParameters the RSASSA-PSS parameters. - */ - public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { - this.rsaSsaPssParameters = rsaSsaPssParameters; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Response of a completed and validated signature session. + */ +public class SignatureResponse implements Serializable { + + private String endResult; + private String signatureValueInBase64; + private String algorithmName; + private SignatureAlgorithm signatureAlgorithm; + private FlowType flowType; + private X509Certificate certificate; + private CertificateLevel requestedCertificateLevel; + private CertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name 'interactionTypeUsed'; Fix in SLIB-138 + private String deviceIpAddress; + private RsaSsaPssParameters rsaSsaPssParameters; + + /** + * Gets the signature value as a byte array by decoding the base64-encoded string. + * + * @return the signature value as a byte array + * @throws UnprocessableSmartIdResponseException if the base64 string is incorrectly encoded + */ + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + /** + * Gets the end result of the signing operation. + *

+ * returns the end result of the signing operation + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the signing operation. + * + * @param endResult the end result of the signing operation + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the signature value as a base64-encoded string. + * + * @return the signature value in base64 + */ + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + /** + * Sets the signature value as a base64-encoded string. + * + * @param signatureValueInBase64 the signature value in base64 + */ + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + /** + * Gets the name of the algorithm used for signing. + * + * @return the name of the algorithm + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Sets the name of the algorithm used for signing. + * + * @param algorithmName the name of the algorithm + */ + public void setAlgorithmName(String algorithmName) { + this.algorithmName = algorithmName; + } + + /** + * Gets the signature algorithm used for signing. + * + * @return the signature algorithm + */ + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Sets the signature algorithm used for signing. + * + * @param signatureAlgorithm the signature algorithm + */ + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Gets the flow type user used to complete the signing. + * + * @return the flow type + */ + public FlowType getFlowType() { + return flowType; + } + + /** + * Sets the flow type. + * + * @param flowType the flow type + */ + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + + /** + * Gets the certificate used for signing. + * + * @return the X.509 certificate + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate used for signing. + * + * @param certificate the X.509 certificate + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the certificate level of the certificate used for signing. + * + * @return the certificate level + */ + public CertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the certificate level of the certificate used for signing. + * + * @param certificateLevel the certificate level + */ + public void setCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the requested certificate level for the signing operation. + * + * @return the requested certificate level + */ + public CertificateLevel getRequestedCertificateLevel() { + return requestedCertificateLevel; + } + + /** + * Sets the requested certificate level for the signing operation. + * + * @param requestedCertificateLevel the requested certificate level + */ + public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { + this.requestedCertificateLevel = requestedCertificateLevel; + } + + /** + * Gets the document number of the user who performed the signing. + * + * @return the document number + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number of the user who performed the signing. + * + * @param documentNumber the document number + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + /** + * Gets the IP address of the device used by the user to complete the signing. + * + * @return the device IP address + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device. + * + * @param deviceIpAddress the device IP address + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + + /** + * Gets the RSASSA-PSS parameters used in the signing operation. + * + * @return the RSASSA-PSS parameters. + */ + public RsaSsaPssParameters getRsaSsaPssParameters() { + return rsaSsaPssParameters; + } + + /** + * Sets the RSASSA-PSS parameters used in the signing operation. + * + * @param rsaSsaPssParameters the RSASSA-PSS parameters. + */ + public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { + this.rsaSsaPssParameters = rsaSsaPssParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index 63b85de4..e2ad7e20 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -1,340 +1,340 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validator for signature session status. - */ -public class SignatureResponseValidator { - - private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); - - private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); - - private final CertificateValidator certificateValidator; - private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; - - /** - * Initializes the validator with a {@link CertificateValidator} and a {@link SignatureCertificatePurposeValidatorFactory}. - * - * @param certificateValidator the certificate validator - * @param signatureCertificatePurposeValidatorFactory the signature certificate purpose validator factory - */ - public SignatureResponseValidator(CertificateValidator certificateValidator, - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; - } - - /** - * Initializes the validator with a {@link CertificateValidator} - * - * @param certificateValidator the certificate validator - */ - public SignatureResponseValidator(CertificateValidator certificateValidator) { - this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates {@link SessionStatus} and produces {@link SignatureResponse}. - * - * @param sessionStatus session status response - * @param requestedCertificateLevel certificate level used to start the signature session - * @return the signature response - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. - * @throws SmartIdClientException if any of method parameters are not provided - */ - public SignatureResponse validate(SessionStatus sessionStatus, - CertificateLevel requestedCertificateLevel - ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - validateSessionsStatus(sessionStatus, requestedCertificateLevel); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate certificate = sessionStatus.getCert(); - - var signatureResponse = new SignatureResponse(); - signatureResponse.setEndResult(sessionResult.getEndResult()); - signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); - signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - - SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - var rsaSsaPssParams = new RsaSsaPssParameters(); - rsaSsaPssParams.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); - rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); - rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); - rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - rsaSsaPssParams.setTrailerField(TrailerField.BC); - signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); - - signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); - signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); - signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); - signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return signatureResponse; - } - - private void validateSessionsStatus(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - - if (StringUtil.isEmpty(sessionStatus.getState())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'state' is empty"); - } - - if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); - } - - validateSessionResult(sessionStatus, requestedCertificateLevel); - } - - private void validateSessionResult(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - SessionResult sessionResult = sessionStatus.getResult(); - - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result' is missing"); - } - - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result.endResult' is empty"); - } - - if ("OK".equalsIgnoreCase(endResult)) { - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result.documentNumber' is empty"); - } - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'interactionTypeUsed' is empty"); - } - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' is empty"); - } - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - validateSignature(sessionStatus); - } else { - ErrorResultHandler.handle(sessionResult); - } - } - - private void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing"); - } - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); - } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); - } - if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Signature session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' has unsupported value"); - } - CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { - logger.error("Signature session status certificate level mismatch: requested {}, returned {}", - requestedCertificateLevel, sessionCertificate.getCertificateLevel()); - throw new CertificateLevelMismatchException(); - } - X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); - certificateValidator.validate(certificate); - - SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); - purposeValidator.validate(certificate); - } - - private static X509Certificate parseAndCheckCertificate(String certBase64) { - X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - logger.error("Signature certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); - throw new UnprocessableSmartIdResponseException("Signature certificate is invalid", ex); - } - return certificate; - } - - private static void validateSignature(SessionStatus sessionStatus) { - String signatureProtocol = sessionStatus.getSignatureProtocol(); - - if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { - validateRawDigestSignature(sessionStatus); - } else { - logger.error("Signature session status field 'signatureProtocol' has unsupported value: {}", signatureProtocol); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' has unsupported value"); - } - } - - private static void validateRawDigestSignature(SessionStatus sessionStatus) { - SessionSignature signature = sessionStatus.getSignature(); - if (signature == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature' is missing"); - } - - validateSignatureValue(signature.getValue()); - validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); - validateFlowType(signature.getFlowType()); - validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); - } - - private static void validateSignatureValue(String value) { - if (StringUtil.isEmpty(value)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' is empty"); - } - if (!BASE64_PATTERN.matcher(value).matches()) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' does not have Base64-encoded value"); - } - } - - private static void validateSignatureAlgorithmName(String signatureAlgorithm) { - if (StringUtil.isEmpty(signatureAlgorithm)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' is missing"); - } - - if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { - List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); - logger.error("Signature session status field 'signature.signatureAlgorithm' has unsupported value: {}. Possible values: {}", signatureAlgorithm, possibleValues); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' has unsupported value"); - } - } - - private static void validateFlowType(String flowType) { - if (StringUtil.isEmpty(flowType)) { - throw new UnprocessableSmartIdResponseException("Signature session status field `signature.flowType` is empty"); - } - if (!FlowType.isSupported(flowType)) { - logger.error("Signature session status field `signature.flowType` has invalid value: {}", flowType); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.flowType' has unsupported value"); - } - } - - private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { - if (sessionSignatureAlgorithmParameters == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters' is missing"); - } - - if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); - } - - Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); - if (hashAlgorithm.isEmpty()) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); - } - - var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); - if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); - } - - if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); - } - - if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"); - } - - if (maskGenAlgorithm.getParameters() == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); - } - - if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); - } - - Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); - if (mgfHashAlgorithm.isEmpty()) { - logger.error("Signature session 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); - } - - if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", - hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); - } - - if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); - } - - int expectedSaltLength = hashAlgorithm.get().getOctetLength(); - int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); - if (expectedSaltLength != actualSaltLength) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected {}, got {}", expectedSaltLength, actualSaltLength); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); - } - - if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); - } - - if (!TrailerField.BC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { - logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validator for signature session status. + */ +public class SignatureResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); + + private final CertificateValidator certificateValidator; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; + + /** + * Initializes the validator with a {@link CertificateValidator} and a {@link SignatureCertificatePurposeValidatorFactory}. + * + * @param certificateValidator the certificate validator + * @param signatureCertificatePurposeValidatorFactory the signature certificate purpose validator factory + */ + public SignatureResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; + } + + /** + * Initializes the validator with a {@link CertificateValidator} + * + * @param certificateValidator the certificate validator + */ + public SignatureResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates {@link SessionStatus} and produces {@link SignatureResponse}. + * + * @param sessionStatus session status response + * @param requestedCertificateLevel certificate level used to start the signature session + * @return the signature response + * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. + * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame + * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code + * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. + * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. + * @throws SmartIdClientException if any of method parameters are not provided + */ + public SignatureResponse validate(SessionStatus sessionStatus, + CertificateLevel requestedCertificateLevel + ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + validateSessionsStatus(sessionStatus, requestedCertificateLevel); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate certificate = sessionStatus.getCert(); + + var signatureResponse = new SignatureResponse(); + signatureResponse.setEndResult(sessionResult.getEndResult()); + signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); + signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var rsaSsaPssParams = new RsaSsaPssParameters(); + rsaSsaPssParams.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rsaSsaPssParams.setTrailerField(TrailerField.BC); + signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); + + signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); + signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); + signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); + signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return signatureResponse; + } + + private void validateSessionsStatus(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + + if (StringUtil.isEmpty(sessionStatus.getState())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'state' is empty"); + } + + if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); + } + + validateSessionResult(sessionStatus, requestedCertificateLevel); + } + + private void validateSessionResult(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + SessionResult sessionResult = sessionStatus.getResult(); + + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result' is missing"); + } + + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.endResult' is empty"); + } + + if ("OK".equalsIgnoreCase(endResult)) { + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.documentNumber' is empty"); + } + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'interactionTypeUsed' is empty"); + } + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' is empty"); + } + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + validateSignature(sessionStatus); + } else { + ErrorResultHandler.handle(sessionResult); + } + } + + private void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); + } + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Signature session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' has unsupported value"); + } + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + logger.error("Signature session status certificate level mismatch: requested {}, returned {}", + requestedCertificateLevel, sessionCertificate.getCertificateLevel()); + throw new CertificateLevelMismatchException(); + } + X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); + certificateValidator.validate(certificate); + + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); + } + + private static X509Certificate parseAndCheckCertificate(String certBase64) { + X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Signature certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Signature certificate is invalid", ex); + } + return certificate; + } + + private static void validateSignature(SessionStatus sessionStatus) { + String signatureProtocol = sessionStatus.getSignatureProtocol(); + + if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { + validateRawDigestSignature(sessionStatus); + } else { + logger.error("Signature session status field 'signatureProtocol' has unsupported value: {}", signatureProtocol); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' has unsupported value"); + } + } + + private static void validateRawDigestSignature(SessionStatus sessionStatus) { + SessionSignature signature = sessionStatus.getSignature(); + if (signature == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature' is missing"); + } + + validateSignatureValue(signature.getValue()); + validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); + validateFlowType(signature.getFlowType()); + validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); + } + + private static void validateSignatureValue(String value) { + if (StringUtil.isEmpty(value)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' is empty"); + } + if (!BASE64_PATTERN.matcher(value).matches()) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' does not have Base64-encoded value"); + } + } + + private static void validateSignatureAlgorithmName(String signatureAlgorithm) { + if (StringUtil.isEmpty(signatureAlgorithm)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' is missing"); + } + + if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { + List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); + logger.error("Signature session status field 'signature.signatureAlgorithm' has unsupported value: {}. Possible values: {}", signatureAlgorithm, possibleValues); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' has unsupported value"); + } + } + + private static void validateFlowType(String flowType) { + if (StringUtil.isEmpty(flowType)) { + throw new UnprocessableSmartIdResponseException("Signature session status field `signature.flowType` is empty"); + } + if (!FlowType.isSupported(flowType)) { + logger.error("Signature session status field `signature.flowType` has invalid value: {}", flowType); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.flowType' has unsupported value"); + } + } + + private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { + if (sessionSignatureAlgorithmParameters == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters' is missing"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); + } + + var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + } + + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"); + } + + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); + } + + Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (mgfHashAlgorithm.isEmpty()) { + logger.error("Signature session 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); + } + + if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", + hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + } + + if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); + } + + int expectedSaltLength = hashAlgorithm.get().getOctetLength(); + int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); + if (expectedSaltLength != actualSaltLength) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected {}, got {}", expectedSaltLength, actualSaltLength); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + } + + if (!TrailerField.BC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { + logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidator.java b/src/main/java/ee/sk/smartid/SignatureValueValidator.java index 5df475af..0197d0ac 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidator.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for signature value validator. - */ -public interface SignatureValueValidator { - - /** - * Validates the signature value against the calculated signature value. - * - * @param signatureValue the signature value to validate - * @param payload the original data that was signed - * @param certificate X509 certificate used for signature validation - * @param rsaSsaPssParameters signature parameters used for creating signature value - * @throws UnprocessableSmartIdResponseException when there are any issue with validating the signature value - */ - void validate(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for signature value validator. + */ +public interface SignatureValueValidator { + + /** + * Validates the signature value against the calculated signature value. + * + * @param signatureValue the signature value to validate + * @param payload the original data that was signed + * @param certificate X509 certificate used for signature validation + * @param rsaSsaPssParameters signature parameters used for creating signature value + * @throws UnprocessableSmartIdResponseException when there are any issue with validating the signature value + */ + void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters); +} diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java index 678f5f9d..ae51a066 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -1,104 +1,104 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.cert.X509Certificate; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Implementation of {@link SignatureValueValidator} that uses RSASSA-PSS signature algorithm - * to validate the signature value in the authentication and signature session status response. - */ -public final class SignatureValueValidatorImpl implements SignatureValueValidator { - - private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); - - @Override - public void validate(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters) { - validateInputs(signatureValue, payload, certificate, rsaSsaPssParameters); - try { - Signature result = getSignature(rsaSsaPssParameters); - result.initVerify(certificate.getPublicKey()); - result.update(payload); - if (!result.verify(signatureValue)) { - throw new UnprocessableSmartIdResponseException("Provided signature value does not match the calculated signature value"); - } - } catch (GeneralSecurityException ex) { - throw new UnprocessableSmartIdResponseException("Signature value validation failed", ex); - } - } - - private Signature getSignature(RsaSsaPssParameters rsaSsaPssParameters) { - try { - var params = new PSSParameterSpec(rsaSsaPssParameters.getDigestHashAlgorithm().getAlgorithmName(), - rsaSsaPssParameters.getMaskGenAlgorithm().getMgfName(), - new MGF1ParameterSpec(rsaSsaPssParameters.getMaskHashAlgorithm().getAlgorithmName()), - rsaSsaPssParameters.getSaltLength(), - rsaSsaPssParameters.getTrailerField().getPssSpecValue()); - var signature = Signature.getInstance(rsaSsaPssParameters.getSignatureAlgorithm().getAlgorithmName()); - signature.setParameter(params); - return signature; - } catch (NoSuchAlgorithmException ex) { - logger.error("Invalid signature algorithm was provided: {}", rsaSsaPssParameters.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); - } catch (InvalidAlgorithmParameterException ex) { - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); - } - } - - private static void validateInputs(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters) { - if (signatureValue == null) { - throw new SmartIdClientException("Parameter 'signatureValue' is not provided"); - } - if (payload == null) { - throw new SmartIdClientException("Parameter 'payload' is not provided"); - } - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - if (rsaSsaPssParameters == null) { - throw new SmartIdClientException("Parameter 'rsaSsaPssParameters' is not provided"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Implementation of {@link SignatureValueValidator} that uses RSASSA-PSS signature algorithm + * to validate the signature value in the authentication and signature session status response. + */ +public final class SignatureValueValidatorImpl implements SignatureValueValidator { + + private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); + + @Override + public void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + validateInputs(signatureValue, payload, certificate, rsaSsaPssParameters); + try { + Signature result = getSignature(rsaSsaPssParameters); + result.initVerify(certificate.getPublicKey()); + result.update(payload); + if (!result.verify(signatureValue)) { + throw new UnprocessableSmartIdResponseException("Provided signature value does not match the calculated signature value"); + } + } catch (GeneralSecurityException ex) { + throw new UnprocessableSmartIdResponseException("Signature value validation failed", ex); + } + } + + private Signature getSignature(RsaSsaPssParameters rsaSsaPssParameters) { + try { + var params = new PSSParameterSpec(rsaSsaPssParameters.getDigestHashAlgorithm().getAlgorithmName(), + rsaSsaPssParameters.getMaskGenAlgorithm().getMgfName(), + new MGF1ParameterSpec(rsaSsaPssParameters.getMaskHashAlgorithm().getAlgorithmName()), + rsaSsaPssParameters.getSaltLength(), + rsaSsaPssParameters.getTrailerField().getPssSpecValue()); + var signature = Signature.getInstance(rsaSsaPssParameters.getSignatureAlgorithm().getAlgorithmName()); + signature.setParameter(params); + return signature; + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", rsaSsaPssParameters.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } catch (InvalidAlgorithmParameterException ex) { + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); + } + } + + private static void validateInputs(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + if (signatureValue == null) { + throw new SmartIdClientException("Parameter 'signatureValue' is not provided"); + } + if (payload == null) { + throw new SmartIdClientException("Parameter 'payload' is not provided"); + } + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + if (rsaSsaPssParameters == null) { + throw new SmartIdClientException("Parameter 'rsaSsaPssParameters' is not provided"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index a3ba1def..89a0a25c 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -1,412 +1,412 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.Configuration; - -/** - * Main entry point for using Smart-ID services. - */ -public class SmartIdClient { - - private String relyingPartyUUID; - private String relyingPartyName; - private String hostUrl; - private Configuration networkConnectionConfig; - private Client configuredClient; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - private SmartIdConnector connector; - private SSLContext trustSslContext; - - private SessionStatusPoller sessionStatusPoller; - - /** - * Creates a new builder for creating a device link certificate choice session request. - * - * @return a builder for creating a new device link certificate choice session request - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertificateRequest() { - return new DeviceLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a linked notification signature session request. - * - * @return a builder for creating a new linked notification signature session request - */ - public LinkedNotificationSignatureSessionRequestBuilder createLinkedNotificationSignature() { - return new LinkedNotificationSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a notification certificate choice session request. - * - * @return a builder for creating a new notification certificate choice session request - */ - public NotificationCertificateChoiceSessionRequestBuilder createNotificationCertificateChoice() { - return new NotificationCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new device link authentication session request - * - * @return builder for creating a new device link authentication session request - */ - public DeviceLinkAuthenticationSessionRequestBuilder createDeviceLinkAuthentication() { - return new DeviceLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new notification authentication session request - * - * @return builder for creating a new notification authentication session request - */ - public NotificationAuthenticationSessionRequestBuilder createNotificationAuthentication() { - return new NotificationAuthenticationSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new device link signature session request - * - * @return builder for creating a new device link signature session request - */ - public DeviceLinkSignatureSessionRequestBuilder createDeviceLinkSignature() { - return new DeviceLinkSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for requesting a certificate using document number. - * - * @return builder for querying certificate using document number - */ - public CertificateByDocumentNumberRequestBuilder createCertificateByDocumentNumber() { - return new CertificateByDocumentNumberRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new notification signature session request - * - * @return builder for creating a new notification signature session request - */ - public NotificationSignatureSessionRequestBuilder createNotificationSignature() { - return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Returns the session status poller or creates a new one if it doesn't exist - * - * @return Sessions status poller - */ - public SessionStatusPoller getSessionStatusPoller() { - if (sessionStatusPoller == null) { - sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); - } - return sessionStatusPoller; - } - - /** - * Create builder for generating device link or QR-code - * - * @return DeviceLinkBuilder - * @throws SmartIdClientException if required parameters are missing or invalid - */ - public DeviceLinkBuilder createDynamicContent() { - return new DeviceLinkBuilder() - .withRelyingPartyName(relyingPartyName); - } - - /** - * Sets the UUID of the relying party - *

- * Can be set also on the builder level, - * but in that case it has to be set explicitly - * every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - */ - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - /** - * Gets the UUID of the relying party - * - * @return UUID of the relying party - */ - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - /** - * Sets the name of the relying party - *

- * Can be set also on the builder level, - * but in that case it has to be set - * every time when building a new request. - * - * @param relyingPartyName name of the relying party - */ - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - /** - * Gets the name of the relying party - * - * @return name of the relying party - */ - public String getRelyingPartyName() { - return relyingPartyName; - } - - /** - * Sets the base URL of the Smart-ID backend environment - *

- * It defines the endpoint which the client communicates to. - * - * @param hostUrl base URL of the Smart-ID backend environment - */ - public void setHostUrl(String hostUrl) { - this.hostUrl = hostUrl; - } - - /** - * Sets the network connection configuration - *

- * Useful for configuring network connection - * timeouts, proxy settings, request headers etc. - * - * @param networkConnectionConfig Jersey's network connection configuration instance - */ - public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { - this.networkConnectionConfig = networkConnectionConfig; - } - - /** - * Set the configured client. - * - * @param configuredClient jakarta.ws.rs.client.Client implementations - */ - public void setConfiguredClient(Client configuredClient) { - this.configuredClient = configuredClient; - } - - /** - * Sets the timeout for each session status poll - *

- * Under the hood each operation (authentication, signing, choosing - * certificate) consists of 2 request steps: - *

- * 1. Initiation request - *

- * 2. Session status request - *

- * Session status request is a long poll method, meaning - * the request method might not return until a timeout expires - * set by this parameter. - *

- * Caller can tune the request parameters inside the bounds - * set by service operator. - *

- * If not provided, a default is used. - * - * @param timeUnit time unit of the {@code timeValue} argument - * @param timeValue time value of each status poll's timeout. - */ - public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - sessionStatusResponseSocketOpenTimeUnit = timeUnit; - sessionStatusResponseSocketOpenTimeValue = timeValue; - } - - /** - * Sets the timeout/pause between each session status poll - * - * @param unit time unit of the {@code timeout} argument - * @param timeout timeout value in the given {@code unit} - */ - public void setPollingSleepTimeout(TimeUnit unit, long timeout) { - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; - } - - /** - * Get smart-id connector. If connector is not set, then new will be created - * - * @return smart-id connector - */ - public SmartIdConnector getSmartIdConnector() { - if (null == connector) { - Client client = configuredClient != null ? configuredClient : createClient(); - SmartIdRestConnector connector = new SmartIdRestConnector(hostUrl, client); - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - - if (trustSslContext == null && configuredClient == null) { - throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); - } - - connector.setSslContext(this.trustSslContext); - setSmartIdConnector(connector); - } - return connector; - } - - /** - * Sets the SSL context for the client - *

- * Useful for configuring custom SSL context - * for the client. - * - * @param trustSslContext SSL context for the client - */ - public void setTrustSslContext(SSLContext trustSslContext) { - this.trustSslContext = trustSslContext; - } - - /** - * Set trust store containing SSL certificates - * - * @param trustStore trust store for the client - */ - public void setTrustStore(KeyStore trustStore) { - try { - SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(trustStore); - trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); - this.trustSslContext = trustSslContext; - } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); - } - } - - /** - * Can be used instead of {@link #setTrustStore} to add SSL certificates. - * - * @param sslCertificates certificates in PEM format - */ - public void setTrustedCertificates(String... sslCertificates) { - try { - this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); - } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Failed to createSslContext", e); - } - } - - /** - * Sets the smart-id connector - *

- * Useful for providing custom implementation - * of the connector. - * - * @param smartIdConnector smart-id connector - */ - public void setSmartIdConnector(SmartIdConnector smartIdConnector) { - this.connector = smartIdConnector; - } - - private Client createClient() { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (networkConnectionConfig != null) { - clientBuilder.withConfig(networkConnectionConfig); - } - if (trustSslContext != null) { - clientBuilder.sslContext(trustSslContext); - } - return clientBuilder.build(); - } - - /** - * Creates an SSL context with the given certificates - * - * @param sslCertificates list of certificates in PEM format - * @return SSL context - * @throws NoSuchAlgorithmException if SSL context with provided protocol is not found - * @throws KeyStoreException if key store cannot be created for a type - * @throws IOException if loading key store fails - * @throws CertificateException if certificate for the given data cannot be generated - * @throws KeyManagementException if SSL context cannot be initialized - */ - public static SSLContext createSslContext(List sslCertificates) - throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - CertificateFactory factory = CertificateFactory.getInstance("X509"); - int i = 0; - for (String sslCertificate : sslCertificates) { - Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); - keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); - } - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(keyStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Configuration; + +/** + * Main entry point for using Smart-ID services. + */ +public class SmartIdClient { + + private String relyingPartyUUID; + private String relyingPartyName; + private String hostUrl; + private Configuration networkConnectionConfig; + private Client configuredClient; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + private long sessionStatusResponseSocketOpenTimeValue; + private SmartIdConnector connector; + private SSLContext trustSslContext; + + private SessionStatusPoller sessionStatusPoller; + + /** + * Creates a new builder for creating a device link certificate choice session request. + * + * @return a builder for creating a new device link certificate choice session request + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a linked notification signature session request. + * + * @return a builder for creating a new linked notification signature session request + */ + public LinkedNotificationSignatureSessionRequestBuilder createLinkedNotificationSignature() { + return new LinkedNotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a notification certificate choice session request. + * + * @return a builder for creating a new notification certificate choice session request + */ + public NotificationCertificateChoiceSessionRequestBuilder createNotificationCertificateChoice() { + return new NotificationCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new device link authentication session request + * + * @return builder for creating a new device link authentication session request + */ + public DeviceLinkAuthenticationSessionRequestBuilder createDeviceLinkAuthentication() { + return new DeviceLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new notification authentication session request + * + * @return builder for creating a new notification authentication session request + */ + public NotificationAuthenticationSessionRequestBuilder createNotificationAuthentication() { + return new NotificationAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new device link signature session request + * + * @return builder for creating a new device link signature session request + */ + public DeviceLinkSignatureSessionRequestBuilder createDeviceLinkSignature() { + return new DeviceLinkSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for requesting a certificate using document number. + * + * @return builder for querying certificate using document number + */ + public CertificateByDocumentNumberRequestBuilder createCertificateByDocumentNumber() { + return new CertificateByDocumentNumberRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new notification signature session request + * + * @return builder for creating a new notification signature session request + */ + public NotificationSignatureSessionRequestBuilder createNotificationSignature() { + return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Returns the session status poller or creates a new one if it doesn't exist + * + * @return Sessions status poller + */ + public SessionStatusPoller getSessionStatusPoller() { + if (sessionStatusPoller == null) { + sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + } + return sessionStatusPoller; + } + + /** + * Create builder for generating device link or QR-code + * + * @return DeviceLinkBuilder + * @throws SmartIdClientException if required parameters are missing or invalid + */ + public DeviceLinkBuilder createDynamicContent() { + return new DeviceLinkBuilder() + .withRelyingPartyName(relyingPartyName); + } + + /** + * Sets the UUID of the relying party + *

+ * Can be set also on the builder level, + * but in that case it has to be set explicitly + * every time when building a new request. + * + * @param relyingPartyUUID UUID of the relying party + */ + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + /** + * Gets the UUID of the relying party + * + * @return UUID of the relying party + */ + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + /** + * Sets the name of the relying party + *

+ * Can be set also on the builder level, + * but in that case it has to be set + * every time when building a new request. + * + * @param relyingPartyName name of the relying party + */ + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + /** + * Gets the name of the relying party + * + * @return name of the relying party + */ + public String getRelyingPartyName() { + return relyingPartyName; + } + + /** + * Sets the base URL of the Smart-ID backend environment + *

+ * It defines the endpoint which the client communicates to. + * + * @param hostUrl base URL of the Smart-ID backend environment + */ + public void setHostUrl(String hostUrl) { + this.hostUrl = hostUrl; + } + + /** + * Sets the network connection configuration + *

+ * Useful for configuring network connection + * timeouts, proxy settings, request headers etc. + * + * @param networkConnectionConfig Jersey's network connection configuration instance + */ + public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { + this.networkConnectionConfig = networkConnectionConfig; + } + + /** + * Set the configured client. + * + * @param configuredClient jakarta.ws.rs.client.Client implementations + */ + public void setConfiguredClient(Client configuredClient) { + this.configuredClient = configuredClient; + } + + /** + * Sets the timeout for each session status poll + *

+ * Under the hood each operation (authentication, signing, choosing + * certificate) consists of 2 request steps: + *

+ * 1. Initiation request + *

+ * 2. Session status request + *

+ * Session status request is a long poll method, meaning + * the request method might not return until a timeout expires + * set by this parameter. + *

+ * Caller can tune the request parameters inside the bounds + * set by service operator. + *

+ * If not provided, a default is used. + * + * @param timeUnit time unit of the {@code timeValue} argument + * @param timeValue time value of each status poll's timeout. + */ + public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + sessionStatusResponseSocketOpenTimeUnit = timeUnit; + sessionStatusResponseSocketOpenTimeValue = timeValue; + } + + /** + * Sets the timeout/pause between each session status poll + * + * @param unit time unit of the {@code timeout} argument + * @param timeout timeout value in the given {@code unit} + */ + public void setPollingSleepTimeout(TimeUnit unit, long timeout) { + pollingSleepTimeUnit = unit; + pollingSleepTimeout = timeout; + } + + /** + * Get smart-id connector. If connector is not set, then new will be created + * + * @return smart-id connector + */ + public SmartIdConnector getSmartIdConnector() { + if (null == connector) { + Client client = configuredClient != null ? configuredClient : createClient(); + SmartIdRestConnector connector = new SmartIdRestConnector(hostUrl, client); + connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + + if (trustSslContext == null && configuredClient == null) { + throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); + } + + connector.setSslContext(this.trustSslContext); + setSmartIdConnector(connector); + } + return connector; + } + + /** + * Sets the SSL context for the client + *

+ * Useful for configuring custom SSL context + * for the client. + * + * @param trustSslContext SSL context for the client + */ + public void setTrustSslContext(SSLContext trustSslContext) { + this.trustSslContext = trustSslContext; + } + + /** + * Set trust store containing SSL certificates + * + * @param trustStore trust store for the client + */ + public void setTrustStore(KeyStore trustStore) { + try { + SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(trustStore); + trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); + this.trustSslContext = trustSslContext; + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); + } + } + + /** + * Can be used instead of {@link #setTrustStore} to add SSL certificates. + * + * @param sslCertificates certificates in PEM format + */ + public void setTrustedCertificates(String... sslCertificates) { + try { + this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); + } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Failed to createSslContext", e); + } + } + + /** + * Sets the smart-id connector + *

+ * Useful for providing custom implementation + * of the connector. + * + * @param smartIdConnector smart-id connector + */ + public void setSmartIdConnector(SmartIdConnector smartIdConnector) { + this.connector = smartIdConnector; + } + + private Client createClient() { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (networkConnectionConfig != null) { + clientBuilder.withConfig(networkConnectionConfig); + } + if (trustSslContext != null) { + clientBuilder.sslContext(trustSslContext); + } + return clientBuilder.build(); + } + + /** + * Creates an SSL context with the given certificates + * + * @param sslCertificates list of certificates in PEM format + * @return SSL context + * @throws NoSuchAlgorithmException if SSL context with provided protocol is not found + * @throws KeyStoreException if key store cannot be created for a type + * @throws IOException if loading key store fails + * @throws CertificateException if certificate for the given data cannot be generated + * @throws KeyManagementException if SSL context cannot be initialized + */ + public static SSLContext createSslContext(List sslCertificates) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + int i = 0; + for (String sslCertificate : sslCertificates) { + Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); + keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(keyStore); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext; + } +} diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index b8482bb8..394968da 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -1,81 +1,81 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response - * and related value for PSSParameterSpec. - */ -public enum TrailerField { - - /** - * Trailer field hexadecimal value "0xbc" with PSSParameterSpec value 1. - */ - BC("0xbc", 1); - - private final String value; - private final int pssSpecValue; - - TrailerField(String value, int pssSpecValue) { - this.value = value; - this.pssSpecValue = pssSpecValue; - } - - /** - * Gets the hexadecimal value of the trailer field. - * - * @return the hexadecimal value of the trailer field. - */ - public String getValue() { - return value; - } - - /** - * Gets the PSSParameterSpec value associated with the trailer field. - * - * @return the PSS specification value. - */ - public int getPssSpecValue() { - return pssSpecValue; - } - - /** - * Converts a string representation of a trailer field to its corresponding TrailerField enum value. - * - * @param trailerField the string representation of the trailer field. - * @return the corresponding TrailerField enum value. - * @throws IllegalArgumentException if the provided string does not match any TrailerField value. - */ - public static TrailerField fromString(String trailerField) { - return Arrays.stream(TrailerField.values()) - .filter(field -> field.getValue().equals(trailerField)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid trailerField value: " + trailerField)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response + * and related value for PSSParameterSpec. + */ +public enum TrailerField { + + /** + * Trailer field hexadecimal value "0xbc" with PSSParameterSpec value 1. + */ + BC("0xbc", 1); + + private final String value; + private final int pssSpecValue; + + TrailerField(String value, int pssSpecValue) { + this.value = value; + this.pssSpecValue = pssSpecValue; + } + + /** + * Gets the hexadecimal value of the trailer field. + * + * @return the hexadecimal value of the trailer field. + */ + public String getValue() { + return value; + } + + /** + * Gets the PSSParameterSpec value associated with the trailer field. + * + * @return the PSS specification value. + */ + public int getPssSpecValue() { + return pssSpecValue; + } + + /** + * Converts a string representation of a trailer field to its corresponding TrailerField enum value. + * + * @param trailerField the string representation of the trailer field. + * @return the corresponding TrailerField enum value. + * @throws IllegalArgumentException if the provided string does not match any TrailerField value. + */ + public static TrailerField fromString(String trailerField) { + return Arrays.stream(TrailerField.values()) + .filter(field -> field.getValue().equals(trailerField)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid trailerField value: " + trailerField)); + } +} diff --git a/src/main/java/ee/sk/smartid/TrustedCACertStore.java b/src/main/java/ee/sk/smartid/TrustedCACertStore.java index c2ebc210..eaef9356 100644 --- a/src/main/java/ee/sk/smartid/TrustedCACertStore.java +++ b/src/main/java/ee/sk/smartid/TrustedCACertStore.java @@ -1,59 +1,59 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -/** - * Interface for a store of trusted CA certificates and trust anchors. - */ -public interface TrustedCACertStore { - - /** - * Get a list of all trusted CA certificates. - * - * @return copy of trusted CA certificates - */ - List getTrustedCACertificates(); - - /** - * Get a set of all trust anchors. - * - * @return copy of trust anchors - */ - Set getTrustAnchors(); - - /** - * Check if OCSP (Online Certificate Status Protocol) validation is enabled. - * - * @return true if OCSP validation is enabled, false otherwise - */ - boolean isOcspEnabled(); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +/** + * Interface for a store of trusted CA certificates and trust anchors. + */ +public interface TrustedCACertStore { + + /** + * Get a list of all trusted CA certificates. + * + * @return copy of trusted CA certificates + */ + List getTrustedCACertificates(); + + /** + * Get a set of all trust anchors. + * + * @return copy of trust anchors + */ + Set getTrustAnchors(); + + /** + * Check if OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @return true if OCSP validation is enabled, false otherwise + */ + boolean isOcspEnabled(); +} diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index 661658f5..00ca2d81 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java @@ -1,65 +1,65 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.ByteBuffer; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for calculating verification code from a hash. - */ -public class VerificationCodeCalculator { - - /** - * The Verification Code (VC) is computed as: - *

- * integer(SHA256(data)[−2:−1]) mod 10000 - *

- * where we take SHA256 result, extract 2 rightmost bytes from it, - * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. - *

- * SHA256 is always used here - * - * @param data byte array to calculate verification code from - * @return verification code. - */ - public static String calculate(byte[] data) { - if (data == null || data.length == 0) { - throw new SmartIdClientException("Parameter 'data' cannot be empty"); - } - byte[] digest = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); - ByteBuffer byteBuffer = ByteBuffer.wrap(digest); - int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 - int rightMostBytesIndex = byteBuffer.limit() - shortBytes; - short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); - int positiveInteger = ((int) twoRightmostBytes) & 0xffff; - String code = String.valueOf(positiveInteger); - String paddedCode = "0000" + code; - return paddedCode.substring(code.length()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.ByteBuffer; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for calculating verification code from a hash. + */ +public class VerificationCodeCalculator { + + /** + * The Verification Code (VC) is computed as: + *

+ * integer(SHA256(data)[−2:−1]) mod 10000 + *

+ * where we take SHA256 result, extract 2 rightmost bytes from it, + * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. + *

+ * SHA256 is always used here + * + * @param data byte array to calculate verification code from + * @return verification code. + */ + public static String calculate(byte[] data) { + if (data == null || data.length == 0) { + throw new SmartIdClientException("Parameter 'data' cannot be empty"); + } + byte[] digest = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); + ByteBuffer byteBuffer = ByteBuffer.wrap(digest); + int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 + int rightMostBytesIndex = byteBuffer.limit() - shortBytes; + short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); + int positiveInteger = ((int) twoRightmostBytes) & 0xffff; + String code = String.valueOf(positiveInteger); + String paddedCode = "0000" + code; + return paddedCode.substring(code.length()); + } +} diff --git a/src/main/java/ee/sk/smartid/VerificationCodeType.java b/src/main/java/ee/sk/smartid/VerificationCodeType.java index 9f5b673f..d542abc1 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeType.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeType.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Verification code types to be used with notification-based authentication request and signing session response - */ -public enum VerificationCodeType { - - NUMERIC4("numeric4"); - - private final String value; - - VerificationCodeType(String value) { - this.value = value; - } - - /** - * Returns the string representation of the verification code type. - * - * @return the string value of the verification code type - */ - public String getValue(){ - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Verification code types to be used with notification-based authentication request and signing session response + */ +public enum VerificationCodeType { + + NUMERIC4("numeric4"); + + private final String value; + + VerificationCodeType(String value) { + this.value = value; + } + + /** + * Returns the string representation of the verification code type. + * + * @return the string value of the verification code type + */ + public String getValue(){ + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java index 4e48ff03..6860047f 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating whether a given X509 certificate is suitable for authentication purposes. - * Implementations should check certificate properties and throw an exception if the certificate is not valid for authentication. - */ -public interface AuthenticationCertificatePurposeValidator { - - /** - * Validates that the provided certificate is suitable for authentication - * - * @param certificate certificate to validate - * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for authentication - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating whether a given X509 certificate is suitable for authentication purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for authentication. + */ +public interface AuthenticationCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for authentication + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for authentication + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java index a35d5b3f..9e183c64 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.AuthenticationCertificateLevel; - -/** - * Factory interface for creating {@link AuthenticationCertificatePurposeValidator} instances - */ -public interface AuthenticationCertificatePurposeValidatorFactory { - - /** - * Creates an {@link AuthenticationCertificatePurposeValidator} for the specified certificate level. - * - * @param certificateLevel the level of the authentication certificate - * @return an instance of {@link AuthenticationCertificatePurposeValidator} suitable for the given level - */ - AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel); -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory interface for creating {@link AuthenticationCertificatePurposeValidator} instances + */ +public interface AuthenticationCertificatePurposeValidatorFactory { + + /** + * Creates an {@link AuthenticationCertificatePurposeValidator} for the specified certificate level. + * + * @param certificateLevel the level of the authentication certificate + * @return an instance of {@link AuthenticationCertificatePurposeValidator} suitable for the given level + */ + AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java index 6e23507e..3b177eae 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java @@ -1,50 +1,50 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.AuthenticationCertificateLevel; - -/** - * Factory implementation for creating {@link AuthenticationCertificatePurposeValidator} - * instances based on the provided {@link AuthenticationCertificateLevel}. - *

- * Returns a validator suitable for the certificate level: - *

    - *
  • {@code QUALIFIED} - returns {@link QualifiedAuthenticationCertificatePurposeValidator}
  • - *
  • {@code ADVANCED} - returns {@link NonQualifiedAuthenticationCertificatePurposeValidator}
  • - *
- */ -public class AuthenticationCertificatePurposeValidatorFactoryImpl implements AuthenticationCertificatePurposeValidatorFactory { - - @Override - public AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel) { - return switch (certificateLevel) { - case QUALIFIED -> new QualifiedAuthenticationCertificatePurposeValidator(); - case ADVANCED -> new NonQualifiedAuthenticationCertificatePurposeValidator(); - }; - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory implementation for creating {@link AuthenticationCertificatePurposeValidator} + * instances based on the provided {@link AuthenticationCertificateLevel}. + *

+ * Returns a validator suitable for the certificate level: + *

    + *
  • {@code QUALIFIED} - returns {@link QualifiedAuthenticationCertificatePurposeValidator}
  • + *
  • {@code ADVANCED} - returns {@link NonQualifiedAuthenticationCertificatePurposeValidator}
  • + *
+ */ +public class AuthenticationCertificatePurposeValidatorFactoryImpl implements AuthenticationCertificatePurposeValidatorFactory { + + @Override + public AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel) { + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedAuthenticationCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedAuthenticationCertificatePurposeValidator(); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java index 41a03d63..d627eb2a 100644 --- a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; -import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Validator for non-qualified Smart-ID authentication certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile - *

- * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. - */ -public class NonQualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { - - @Override - public void validate(X509Certificate certificate) { - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - NonQualifiedSmartIdCertificateValidator.validate(certificate); - SmartIdAuthenticationCertificateValidator.validate(certificate); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Validator for non-qualified Smart-ID authentication certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + *

+ * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class NonQualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + NonQualifiedSmartIdCertificateValidator.validate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } +} diff --git a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java index 1bcc7150..5ef6f239 100644 --- a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java @@ -1,77 +1,77 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the authentication certificate is a qualified Smart-ID certificate and can be used for authentication. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Authentication - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (authentication) for Qualified profile - *

- * * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. - */ -public class QualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { - - private final Logger logger = LoggerFactory.getLogger(QualifiedAuthenticationCertificatePurposeValidator.class); - - private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.2042.1.2"); - - @Override - public void validate(X509Certificate certificate) { - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - validateCertificateIsQualifiedSmartIdCertificate(certificate); - SmartIdAuthenticationCertificateValidator.validate(certificate); - } - - private void validateCertificateIsQualifiedSmartIdCertificate(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate"); - } - if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate is not a qualified Smart-ID authentication certificate"); - } - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the authentication certificate is a qualified Smart-ID certificate and can be used for authentication. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Authentication + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (authentication) for Qualified profile + *

+ * * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class QualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + private final Logger logger = LoggerFactory.getLogger(QualifiedAuthenticationCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.2042.1.2"); + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + validateCertificateIsQualifiedSmartIdCertificate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } + + private void validateCertificateIsQualifiedSmartIdCertificate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a qualified Smart-ID authentication certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionType.java b/src/main/java/ee/sk/smartid/common/InteractionType.java index 4063127d..54210156 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionType.java +++ b/src/main/java/ee/sk/smartid/common/InteractionType.java @@ -1,47 +1,47 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Interface for interaction types that can be used in authentication and signing requests. - */ -public interface InteractionType { - - /** - * Get the interaction type as value that can be used in the Smart ID API. - * - * @return code representing the interaction type - */ - String getCode(); - - /** - * Get the maximum length of the display text for this interaction type. - * - * @return maximum length of the display text - */ - int getMaxLength(); -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Interface for interaction types that can be used in authentication and signing requests. + */ +public interface InteractionType { + + /** + * Get the interaction type as value that can be used in the Smart ID API. + * + * @return code representing the interaction type + */ + String getCode(); + + /** + * Get the maximum length of the display text for this interaction type. + * + * @return maximum length of the display text + */ + int getMaxLength(); +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionValidator.java b/src/main/java/ee/sk/smartid/common/InteractionValidator.java index 21873f3e..64036156 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionValidator.java +++ b/src/main/java/ee/sk/smartid/common/InteractionValidator.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.util.StringUtil; - -/** - * Validator for interactions - */ -public final class InteractionValidator { - - private InteractionValidator() { - } - - /** - * Validates that the text is set and does not exceed the maximum length defined by the type - * - * @param type the type to be validated - * @param text the text to be validated - * @param implementation of InteractionType - */ - public static void validate(T type, String text) { - if (StringUtil.isEmpty(text)) { - throw new SmartIdRequestSetupException(String.format("Value for '%s' must be set when type is '%s'", "displayText" + type.getMaxLength(), type)); - } - if (text.length() > type.getMaxLength()) { - throw new SmartIdRequestSetupException(String.format("Value for '%s' must not exceed %d characters", "displayText" + type.getMaxLength(), type.getMaxLength())); - } - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.util.StringUtil; + +/** + * Validator for interactions + */ +public final class InteractionValidator { + + private InteractionValidator() { + } + + /** + * Validates that the text is set and does not exceed the maximum length defined by the type + * + * @param type the type to be validated + * @param text the text to be validated + * @param implementation of InteractionType + */ + public static void validate(T type, String text) { + if (StringUtil.isEmpty(text)) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must be set when type is '%s'", "displayText" + type.getMaxLength(), type)); + } + if (text.length() > type.getMaxLength()) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must not exceed %d characters", "displayText" + type.getMaxLength(), type.getMaxLength())); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java index c56e52ae..175f1020 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Objects; - -import ee.sk.smartid.rest.dao.Interaction; - -/** - * Mapper form converting between different interaction representations - */ -public final class InteractionsMapper { - - private InteractionsMapper() { - } - - /** - * Converts from any SmartIdInteraction to Interaction - * - * @param type of SmartIdInteraction - * @param interaction the interaction to be converted - * @return interaction to be used in REST request - */ - public static Interaction from(T interaction) { - return new Interaction(interaction.type().getCode(), interaction.displayText60(), interaction.displayText200()); - } - - /** - * Converts from any list of SmartIdInteraction to list of Interaction - * - * @param interactions the interactions to be converted - * @return list of interactions to be used in REST request - */ - public static List from(List interactions) { - return interactions.stream().filter(Objects::nonNull).map(InteractionsMapper::from).toList(); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Objects; + +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Mapper form converting between different interaction representations + */ +public final class InteractionsMapper { + + private InteractionsMapper() { + } + + /** + * Converts from any SmartIdInteraction to Interaction + * + * @param type of SmartIdInteraction + * @param interaction the interaction to be converted + * @return interaction to be used in REST request + */ + public static Interaction from(T interaction) { + return new Interaction(interaction.type().getCode(), interaction.displayText60(), interaction.displayText200()); + } + + /** + * Converts from any list of SmartIdInteraction to list of Interaction + * + * @param interactions the interactions to be converted + * @return list of interactions to be used in REST request + */ + public static List from(List interactions) { + return interactions.stream().filter(Objects::nonNull).map(InteractionsMapper::from).toList(); + } +} diff --git a/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java index 9a4e7d22..108b035e 100644 --- a/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java +++ b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Interaction to be used in authentication and signing requests - */ -public interface SmartIdInteraction { - - /** - * Gets the interaction type - * - * @return the interaction type - */ - InteractionType type(); - - /** - * Gets the text to be displayed on the device screen (maximum length 60 characters). - * - * @return the text to be displayed on the device screen (maximum length 60 characters). - */ - String displayText60(); - - /** - * Gets the text to be displayed on the device screen (maximum length 200 characters). - * - * @return the text to be displayed on the device screen (maximum length 200 characters). - */ - String displayText200(); -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Interaction to be used in authentication and signing requests + */ +public interface SmartIdInteraction { + + /** + * Gets the interaction type + * + * @return the interaction type + */ + InteractionType type(); + + /** + * Gets the text to be displayed on the device screen (maximum length 60 characters). + * + * @return the text to be displayed on the device screen (maximum length 60 characters). + */ + String displayText60(); + + /** + * Gets the text to be displayed on the device screen (maximum length 200 characters). + * + * @return the text to be displayed on the device screen (maximum length 200 characters). + */ + String displayText200(); +} diff --git a/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java index 5dbf7389..3d38c38c 100644 --- a/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java @@ -1,72 +1,72 @@ -package ee.sk.smartid.common.certificate; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validator for non-qualified Smart-ID certificates. Can be used for both authentication and signing certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) and (authentification) for Non-Qualified profile - */ -public final class NonQualifiedSmartIdCertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSmartIdCertificateValidator.class); - - private static final Set NON_QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); - - private NonQualifiedSmartIdCertificateValidator() { - } - - /** - * Validates that the provided certificate is a non-qualified Smart-ID certificate. - * - * @param certificate the certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate is not a non-qualified Smart-ID certificate - */ - public static void validate(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate"); - } - if (!certificatePolicyOids.containsAll(NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate is not a non-qualified Smart-ID certificate"); - } - } -} +package ee.sk.smartid.common.certificate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validator for non-qualified Smart-ID certificates. Can be used for both authentication and signing certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) and (authentification) for Non-Qualified profile + */ +public final class NonQualifiedSmartIdCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSmartIdCertificateValidator.class); + + private static final Set NON_QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); + + private NonQualifiedSmartIdCertificateValidator() { + } + + /** + * Validates that the provided certificate is a non-qualified Smart-ID certificate. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is not a non-qualified Smart-ID certificate + */ + public static void validate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate"); + } + if (!certificatePolicyOids.containsAll(NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a non-qualified Smart-ID certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java index 716045ab..10d901ec 100644 --- a/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java @@ -1,113 +1,113 @@ -package ee.sk.smartid.common.certificate; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Validator for Smart-ID authentication certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified and Non-Qualified and Digital authentication - */ -public final class SmartIdAuthenticationCertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(SmartIdAuthenticationCertificateValidator.class); - - private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; - private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; - private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; - - private SmartIdAuthenticationCertificateValidator() { - } - - /** - * Validates that the provided certificate can be used for authentication. - * - * @param certificate the certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate cannot be used for authentication - */ - public static void validate(X509Certificate certificate) { - if (!(isAfterApril2025Certificates(certificate) || isBeforeApril2025Certificates(certificate))) { - throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); - } - } - - // From April 2025 forward - // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 - // KeyUsage - digitalSignature - private static boolean isAfterApril2025Certificates(X509Certificate certificate) { - if (!hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { - return false; - } - boolean[] keyUsage = certificate.getKeyUsage(); - if (!(keyUsage != null && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE])) { - logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); - return false; - } - return true; - } - - // Before April 2025 - // Extended key usage - 1.3.6.1.5.5.7.3.2 - // Key Usage - digitalSignature, keyEncipherment, dataEncipherment - private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { - if (!hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { - return false; - } - boolean[] keyUsage = certificate.getKeyUsage(); - if (!(keyUsage != null - && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] - && keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] - && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { - logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); - return false; - } - return true; - } - - private static boolean hasExtendedKey(X509Certificate certificate, String oid) { - try { - List extendedKeyUsage = certificate.getExtendedKeyUsage(); - if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { - logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); - return false; - } - } catch (CertificateParsingException ex) { - throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); - } - return true; - } -} +package ee.sk.smartid.common.certificate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Validator for Smart-ID authentication certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified and Non-Qualified and Digital authentication + */ +public final class SmartIdAuthenticationCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(SmartIdAuthenticationCertificateValidator.class); + + private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; + private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; + private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; + + private SmartIdAuthenticationCertificateValidator() { + } + + /** + * Validates that the provided certificate can be used for authentication. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate cannot be used for authentication + */ + public static void validate(X509Certificate certificate) { + if (!(isAfterApril2025Certificates(certificate) || isBeforeApril2025Certificates(certificate))) { + throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); + } + } + + // From April 2025 forward + // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 + // KeyUsage - digitalSignature + private static boolean isAfterApril2025Certificates(X509Certificate certificate) { + if (!hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + // Before April 2025 + // Extended key usage - 1.3.6.1.5.5.7.3.2 + // Key Usage - digitalSignature, keyEncipherment, dataEncipherment + private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { + if (!hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null + && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] + && keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] + && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + private static boolean hasExtendedKey(X509Certificate certificate, String oid) { + try { + List extendedKeyUsage = certificate.getExtendedKeyUsage(); + if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { + logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); + return false; + } + } catch (CertificateParsingException ex) { + throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); + } + return true; + } +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java index 3fd6c9d3..63916137 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.net.URI; - -/** - * Represents a callback URL with an associated URL-safe token. - * - * @param initialCallbackUri the full callback URI including the token as a query parameter - * @param urlToken the generated URL-safe token - */ -public record CallbackUrl(URI initialCallbackUri, String urlToken) { -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; + +/** + * Represents a callback URL with an associated URL-safe token. + * + * @param initialCallbackUri the full callback URI including the token as a query parameter + * @param urlToken the generated URL-safe token + */ +public record CallbackUrl(URI initialCallbackUri, String urlToken) { +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java index 4ff0cf07..c700229d 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java @@ -1,85 +1,85 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.SecureRandom; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Generates URL-safe tokens using a cryptographically secure random number generator. - */ -public class UrlSafeTokenGenerator { - - private static final int MIN_NR_OF_CHARACTERS = 22; - private static final int MAX_NR_OF_CHARACTERS = 86; - - private UrlSafeTokenGenerator() { - } - - /** - * Generates a random URL-safe token between 22 and 86 characters long. - * - * @return a random URL-safe token with random size between the specified lengths - */ - public static String random() { - return randomBetween(MIN_NR_OF_CHARACTERS, MAX_NR_OF_CHARACTERS); - } - - /** - * Generates a random URL-safe token of the specified length. - * - * @param length the length of the token to generate (must be between 22 and 86) - * @return a random URL-safe token of the specified length - */ - public static String ofLength(int length) { - return randomBetween(length, length); - } - - /** - * Generates a random URL-safe token between the specified minimum and maximum lengths. - * - * @param minLen the minimum length of the token (must be between 22 and 86) - * @param maxLen the maximum length of the token (must be between 22 and 86) - * @return a random URL-safe token with random size between the specified lengths - * @throws SmartIdClientException if the specified lengths are out of bounds or invalid - */ - public static String randomBetween(int minLen, int maxLen) { - if (minLen < MIN_NR_OF_CHARACTERS || maxLen > MAX_NR_OF_CHARACTERS || minLen > maxLen) { - throw new SmartIdClientException("Length must be between 22 and 86 chars"); - } - SecureRandom secureRandom = new SecureRandom(); - // Random length between minLen and maxLen (inclusive) - int targetLen = secureRandom.nextInt(maxLen - minLen + 1) + minLen; - byte[] bytes = new byte[64]; - secureRandom.nextBytes(bytes); - String random = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); - // Trim down to desired length - return random.substring(0, targetLen); - } -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Generates URL-safe tokens using a cryptographically secure random number generator. + */ +public class UrlSafeTokenGenerator { + + private static final int MIN_NR_OF_CHARACTERS = 22; + private static final int MAX_NR_OF_CHARACTERS = 86; + + private UrlSafeTokenGenerator() { + } + + /** + * Generates a random URL-safe token between 22 and 86 characters long. + * + * @return a random URL-safe token with random size between the specified lengths + */ + public static String random() { + return randomBetween(MIN_NR_OF_CHARACTERS, MAX_NR_OF_CHARACTERS); + } + + /** + * Generates a random URL-safe token of the specified length. + * + * @param length the length of the token to generate (must be between 22 and 86) + * @return a random URL-safe token of the specified length + */ + public static String ofLength(int length) { + return randomBetween(length, length); + } + + /** + * Generates a random URL-safe token between the specified minimum and maximum lengths. + * + * @param minLen the minimum length of the token (must be between 22 and 86) + * @param maxLen the maximum length of the token (must be between 22 and 86) + * @return a random URL-safe token with random size between the specified lengths + * @throws SmartIdClientException if the specified lengths are out of bounds or invalid + */ + public static String randomBetween(int minLen, int maxLen) { + if (minLen < MIN_NR_OF_CHARACTERS || maxLen > MAX_NR_OF_CHARACTERS || minLen > maxLen) { + throw new SmartIdClientException("Length must be between 22 and 86 chars"); + } + SecureRandom secureRandom = new SecureRandom(); + // Random length between minLen and maxLen (inclusive) + int targetLen = secureRandom.nextInt(maxLen - minLen + 1) + minLen; + byte[] bytes = new byte[64]; + secureRandom.nextBytes(bytes); + String random = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + // Trim down to desired length + return random.substring(0, targetLen); + } +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java index 29803eee..55db8d75 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java @@ -1,87 +1,87 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionValidator; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * Interaction to be used in device-link based authentication and signing requests - * - * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ -public record DeviceLinkInteraction(DeviceLinkInteractionType type, - String displayText60, - String displayText200) implements SmartIdInteraction { - - /** - * Creates a new instance of {@link DeviceLinkInteraction}. - *

- * Display text fields will be validated based on interaction type. - * - * @param type interaction type (see {@link DeviceLinkInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ - public DeviceLinkInteraction { - if (type == null) { - throw new SmartIdRequestSetupException("Value for 'type' must be set"); - } - if (type == DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN) { - InteractionValidator.validate(type, displayText60); - } - if (type == DeviceLinkInteractionType.CONFIRMATION_MESSAGE) { - InteractionValidator.validate(type, displayText200); - } - } - - /** - * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#DISPLAY_TEXT_AND_PIN} - * - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @return instance of DeviceLinkInteraction - * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type - */ - public static DeviceLinkInteraction displayTextAndPin(String displayText60) { - return new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); - } - - /** - * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#CONFIRMATION_MESSAGE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return instance of DeviceLinkInteraction - * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type - */ - public static DeviceLinkInteraction confirmationMessage(String displayText200) { - return new DeviceLinkInteraction(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, null, displayText200); - } -} - +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * Interaction to be used in device-link based authentication and signing requests + * + * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record DeviceLinkInteraction(DeviceLinkInteractionType type, + String displayText60, + String displayText200) implements SmartIdInteraction { + + /** + * Creates a new instance of {@link DeviceLinkInteraction}. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type interaction type (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ + public DeviceLinkInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == DeviceLinkInteractionType.CONFIRMATION_MESSAGE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction displayTextAndPin(String displayText60) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction confirmationMessage(String displayText200) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } +} + diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java index 2c1c1bda..1b33345c 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionType; - -/** - * Interaction types that can be used in device link-based authentication and signing requests - */ -public enum DeviceLinkInteractionType implements InteractionType { - - /** - * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. - */ - DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. - */ - CONFIRMATION_MESSAGE("confirmationMessage", 200); - - private final String code; - private final int maxLength; - - DeviceLinkInteractionType(String code, int maxLength) { - this.code = code; - this.maxLength = maxLength; - } - - @Override - public String getCode() { - return code; - } - - @Override - public int getMaxLength() { - return maxLength; - } -} +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionType; + +/** + * Interaction types that can be used in device link-based authentication and signing requests + */ +public enum DeviceLinkInteractionType implements InteractionType { + + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ + CONFIRMATION_MESSAGE("confirmationMessage", 200); + + private final String code; + private final int maxLength; + + DeviceLinkInteractionType(String code, int maxLength) { + this.code = code; + this.maxLength = maxLength; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMaxLength() { + return maxLength; + } +} diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java index 7981f9fb..9db4daa6 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java @@ -1,98 +1,98 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import ee.sk.smartid.common.InteractionValidator; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * Interaction to be used in notification-based authentication and signing requests - * - * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ -public record NotificationInteraction(NotificationInteractionType type, - String displayText60, - String displayText200) implements Serializable, SmartIdInteraction { - - /** - * Constructs a new NotificationInteraction instance. - *

- * Display text fields will be validated based on interaction type. - * - * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @throws SmartIdRequestSetupException if display text fields have incorrect value based on the type - */ - public NotificationInteraction { - if (type == null) { - throw new SmartIdRequestSetupException("Value for 'type' must be set"); - } - if (type == NotificationInteractionType.DISPLAY_TEXT_AND_PIN) { - InteractionValidator.validate(type, displayText60); - } - if (type == NotificationInteractionType.CONFIRMATION_MESSAGE - || type == NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { - InteractionValidator.validate(type, displayText200); - } - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#DISPLAY_TEXT_AND_PIN} - * - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @return the interaction - */ - public static NotificationInteraction displayTextAndPin(String displayText60) { - return new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return the interaction - */ - public static NotificationInteraction confirmationMessage(String displayText200) { - return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE, null, displayText200); - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return the interaction - */ - public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { - return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, null, displayText200); - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * Interaction to be used in notification-based authentication and signing requests + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record NotificationInteraction(NotificationInteractionType type, + String displayText60, + String displayText200) implements Serializable, SmartIdInteraction { + + /** + * Constructs a new NotificationInteraction instance. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @throws SmartIdRequestSetupException if display text fields have incorrect value based on the type + */ + public NotificationInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == NotificationInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == NotificationInteractionType.CONFIRMATION_MESSAGE + || type == NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return the interaction + */ + public static NotificationInteraction displayTextAndPin(String displayText60) { + return new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessage(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, null, displayText200); + } +} diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java index 80f88363..3ae5ec32 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java @@ -1,66 +1,66 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionType; - -/** - * Interaction types that can be used in notification-based authentication and signing requests - */ -public enum NotificationInteractionType implements InteractionType { - - /** - * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. - */ - DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. - */ - CONFIRMATION_MESSAGE("confirmationMessage", 200), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog and verification code choice before entering the PIN. - */ - CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); - - private final String code; - private final int maxLength; - - NotificationInteractionType(String code, int maxLength) { - this.code = code; - this.maxLength = maxLength; - } - - @Override - public String getCode() { - return code; - } - - @Override - public int getMaxLength() { - return maxLength; - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionType; + +/** + * Interaction types that can be used in notification-based authentication and signing requests + */ +public enum NotificationInteractionType implements InteractionType { + + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ + CONFIRMATION_MESSAGE("confirmationMessage", 200), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog and verification code choice before entering the PIN. + */ + CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); + + private final String code; + private final int maxLength; + + NotificationInteractionType(String code, int maxLength) { + this.code = code; + this.maxLength = maxLength; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMaxLength() { + return maxLength; + } +} diff --git a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java index fb625ec6..376f81cd 100644 --- a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exceptions that subclass this mark situations where something is wrong with - * client-side integration or how Relying Party account has been configured by Smart-ID operator - * or Smart-ID server is under maintenance. - * With these types of errors there is not recommended to ask the user for immediate retry. - */ -public abstract class EnduringSmartIdException extends SmartIdException { - - /** - * Constructs the exception with the specified message. - * - * @param message the message to describe the reason for the exception - */ - public EnduringSmartIdException(String message) { - super(message); - } - - /** - * Constructs a new exception with the specified message and cause. - * - * @param message the message to describe the reason for the exception - * @param cause the underlying cause of the exception - */ - public EnduringSmartIdException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exceptions that subclass this mark situations where something is wrong with + * client-side integration or how Relying Party account has been configured by Smart-ID operator + * or Smart-ID server is under maintenance. + * With these types of errors there is not recommended to ask the user for immediate retry. + */ +public abstract class EnduringSmartIdException extends SmartIdException { + + /** + * Constructs the exception with the specified message. + * + * @param message the message to describe the reason for the exception + */ + public EnduringSmartIdException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message to describe the reason for the exception + * @param cause the underlying cause of the exception + */ + public EnduringSmartIdException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java index e126e25e..7329a81d 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java @@ -1,33 +1,33 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session with the given session ID could not be found. - */ -public class SessionNotFoundException extends SmartIdException { -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session with the given session ID could not be found. + */ +public class SessionNotFoundException extends SmartIdException { +} diff --git a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java index 5736ac83..6686e884 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when the session secret digest from the callback does not match the calculated digest. - */ -public class SessionSecretMismatchException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SessionSecretMismatchException(String message) { - super(message); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when the session secret digest from the callback does not match the calculated digest. + */ +public class SessionSecretMismatchException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SessionSecretMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/SmartIdException.java b/src/main/java/ee/sk/smartid/exception/SmartIdException.java index b6fd4b2f..ade03ce5 100644 --- a/src/main/java/ee/sk/smartid/exception/SmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/SmartIdException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * All Smart-ID exceptions subclass from this. - */ -public abstract class SmartIdException extends RuntimeException { - - public SmartIdException() { - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the underlying cause of this exception. - */ - public SmartIdException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * All Smart-ID exceptions subclass from this. + */ +public abstract class SmartIdException extends RuntimeException { + + public SmartIdException() { + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the underlying cause of this exception. + */ + public SmartIdException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java index 57c4193a..42bd0029 100644 --- a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java +++ b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Thrown when validation of any Smart-ID API responses fail. - * This includes responses for session initialization requests and session status responses. - */ -public class UnprocessableSmartIdResponseException extends SmartIdClientException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UnprocessableSmartIdResponseException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param exception the exception that caused this exception to be thrown. - */ - public UnprocessableSmartIdResponseException(String message, Exception exception) { - super(message, exception); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Thrown when validation of any Smart-ID API responses fail. + * This includes responses for session initialization requests and session status responses. + */ +public class UnprocessableSmartIdResponseException extends SmartIdClientException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UnprocessableSmartIdResponseException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param exception the exception that caused this exception to be thrown. + */ + public UnprocessableSmartIdResponseException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/UserAccountException.java b/src/main/java/ee/sk/smartid/exception/UserAccountException.java index d964f758..8b1a5560 100644 --- a/src/main/java/ee/sk/smartid/exception/UserAccountException.java +++ b/src/main/java/ee/sk/smartid/exception/UserAccountException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Subclasses of this exception are situation where something is wrong with user's Smart-ID account (or app) configuration. - * General practise is to display a notification and ask user to log in to Smart-ID self-service portal. - */ -public abstract class UserAccountException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserAccountException(String message) { - super(message); - } - -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Subclasses of this exception are situation where something is wrong with user's Smart-ID account (or app) configuration. + * General practise is to display a notification and ask user to log in to Smart-ID self-service portal. + */ +public abstract class UserAccountException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserAccountException(String message) { + super(message); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/UserActionException.java b/src/main/java/ee/sk/smartid/exception/UserActionException.java index 56cf6cd0..6ac7e5eb 100644 --- a/src/main/java/ee/sk/smartid/exception/UserActionException.java +++ b/src/main/java/ee/sk/smartid/exception/UserActionException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Subclasses of this exception are situation where user's action triggered ending session. - * General practise is to ask the user to try again. - */ -public abstract class UserActionException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserActionException(String message) { - super(message); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Subclasses of this exception are situation where user's action triggered ending session. + * General practise is to ask the user to try again. + */ +public abstract class UserActionException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserActionException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java index 00372c2f..81958825 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Linked signature flow consists of two sessions - device link-based certificate choice session followed by the linked signature session. - * Exception will be thrown when linked signature session is not received after the device link-based certificate choice session, - * but some other session with the same document number is received instead. - */ -public class ExpectedLinkedSessionException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ExpectedLinkedSessionException() { - super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Linked signature flow consists of two sessions - device link-based certificate choice session followed by the linked signature session. + * Exception will be thrown when linked signature session is not received after the device link-based certificate choice session, + * but some other session with the same document number is received instead. + */ +public class ExpectedLinkedSessionException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ExpectedLinkedSessionException() { + super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java index bb262cd7..645c52a7 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Exception thrown when the session status end result is PROTOCOL_FAILURE, indicating logical error in the signing protocol. - *

- * F.e. Constructed device link that user can interact with contains invalid schema. - */ -public class ProtocolFailureException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ProtocolFailureException() { - super("A logical error occurred in the signing protocol."); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Exception thrown when the session status end result is PROTOCOL_FAILURE, indicating logical error in the signing protocol. + *

+ * F.e. Constructed device link that user can interact with contains invalid schema. + */ +public class ProtocolFailureException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ProtocolFailureException() { + super("A logical error occurred in the signing protocol."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java index d618d1f9..760f143e 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exception will be thrown when there are problems with relying party account and access configuration - * or when relying party does not have access to the requested service. - *

- * F.e. Request is made with relying party UUID and incorrect relying party name. - */ -public class RelyingPartyAccountConfigurationException extends SmartIdClientException { - - /** - * Constructs the exception with message and cause. - * - * @param message the exception message - * @param exception underlying cause for this exception - */ - public RelyingPartyAccountConfigurationException(String message, Exception exception) { - super(message, exception); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exception will be thrown when there are problems with relying party account and access configuration + * or when relying party does not have access to the requested service. + *

+ * F.e. Request is made with relying party UUID and incorrect relying party name. + */ +public class RelyingPartyAccountConfigurationException extends SmartIdClientException { + + /** + * Constructs the exception with message and cause. + * + * @param message the exception message + * @param exception underlying cause for this exception + */ + public RelyingPartyAccountConfigurationException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java index fc46fbd6..16f1708d 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Thrown when request cannot be process because the Smart-ID API server is under maintenance. - */ -public class ServerMaintenanceException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ServerMaintenanceException() { - super("Server is under maintenance, retry later."); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Thrown when request cannot be process because the Smart-ID API server is under maintenance. + */ +public class ServerMaintenanceException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ServerMaintenanceException() { + super("Server is under maintenance, retry later."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java index e40589e8..b20bf311 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * This exception is thrown if client (this library) has configuration errors. - */ -public class SmartIdClientException extends EnduringSmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdClientException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the exception that caused this exception to be thrown. - */ - public SmartIdClientException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * This exception is thrown if client (this library) has configuration errors. + */ +public class SmartIdClientException extends EnduringSmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdClientException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ + public SmartIdClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java index 3cf49946..4333c0b2 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exception thrown when there is an issue setting up a Smart-ID request. - * This could be due to invalid parameters, configuration issues, or other - * problems that prevent from successfully preparing the request. - */ -public class SmartIdRequestSetupException extends SmartIdClientException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdRequestSetupException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the exception that caused this exception to be thrown. - */ - public SmartIdRequestSetupException(String message, Exception cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exception thrown when there is an issue setting up a Smart-ID request. + * This could be due to invalid parameters, configuration issues, or other + * problems that prevent from successfully preparing the request. + */ +public class SmartIdRequestSetupException extends SmartIdClientException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdRequestSetupException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ + public SmartIdRequestSetupException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java index b0d84fe5..d082abdc 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Thrown when session status end result is SERVER_ERROR, indicating a server-side technical error. - */ -public class SmartIdServerException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public SmartIdServerException() { - super("Process was terminated due to server-side technical error"); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Thrown when session status end result is SERVER_ERROR, indicating a server-side technical error. + */ +public class SmartIdServerException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public SmartIdServerException() { + super("Process was terminated due to server-side technical error"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java index 4db92fb2..f00d8ff6 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java @@ -1,51 +1,51 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when returned certificate level is lower than the requested certificate level. - */ -public class CertificateLevelMismatchException extends UserAccountException { - - /** - * Constructs the exception with the default message. - */ - public CertificateLevelMismatchException() { - super("Signer's certificate is below requested certificate level"); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message - */ - public CertificateLevelMismatchException(String message) { - super(message); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when returned certificate level is lower than the requested certificate level. + */ +public class CertificateLevelMismatchException extends UserAccountException { + + /** + * Constructs the exception with the default message. + */ + public CertificateLevelMismatchException() { + super("Signer's certificate is below requested certificate level"); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message + */ + public CertificateLevelMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java index 2df63e82..c3bb3616 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java @@ -1,40 +1,40 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is DOCUMENT_UNUSABLE. - */ -public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { - - /** - * Constructs the exception with default message. - */ - public DocumentUnusableException() { - super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is DOCUMENT_UNUSABLE. + */ +public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { + + /** + * Constructs the exception with default message. + */ + public DocumentUnusableException() { + super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java index 86a1d0e6..c4ed6a99 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when user does not have a suitable account for the requested operation. - *

- * F.e. user has non-qualified account with ADVANCED certificate level, - * but QUALIFIED certificate level is required for the operation. - */ -public class NoSuitableAccountOfRequestedTypeFoundException extends UserAccountException { - - /** - * Constructs the exception with default message. - */ - public NoSuitableAccountOfRequestedTypeFoundException() { - super("No suitable account of requested type found, but user has some other accounts."); - } - -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when user does not have a suitable account for the requested operation. + *

+ * F.e. user has non-qualified account with ADVANCED certificate level, + * but QUALIFIED certificate level is required for the operation. + */ +public class NoSuitableAccountOfRequestedTypeFoundException extends UserAccountException { + + /** + * Constructs the exception with default message. + */ + public NoSuitableAccountOfRequestedTypeFoundException() { + super("No suitable account of requested type found, but user has some other accounts."); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java index 1a3b0b87..98b06b2d 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java @@ -1,51 +1,51 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when Smart-ID API indicates that there is an issue with user document and user should check its state. - */ -public class PersonShouldViewSmartIdPortalException extends UserAccountException { - - /** - * Constructs the exception with default message. - */ - public PersonShouldViewSmartIdPortalException() { - super("Person should view Smart-ID app or Smart-ID self-service portal now."); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message exception message - */ - public PersonShouldViewSmartIdPortalException(String message) { - super(message); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when Smart-ID API indicates that there is an issue with user document and user should check its state. + */ +public class PersonShouldViewSmartIdPortalException extends UserAccountException { + + /** + * Constructs the exception with default message. + */ + public PersonShouldViewSmartIdPortalException() { + super("Person should view Smart-ID app or Smart-ID self-service portal now."); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message exception message + */ + public PersonShouldViewSmartIdPortalException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java index c2b9a14c..16470be2 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when the user's app version does not support any of the provided interactions. - */ -public class RequiredInteractionNotSupportedByAppException extends UserAccountException { - - /** - * Constructs the exception with the default message. - */ - public RequiredInteractionNotSupportedByAppException() { - super("User app version does not support any of the provided interactions."); - } - -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when the user's app version does not support any of the provided interactions. + */ +public class RequiredInteractionNotSupportedByAppException extends UserAccountException { + + /** + * Constructs the exception with the default message. + */ + public RequiredInteractionNotSupportedByAppException() { + super("User app version does not support any of the provided interactions."); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java index 725af6ec..74435578 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when user account does not exist with the given identifier or document number. - */ -public class UserAccountNotFoundException extends UserAccountException { - - /** - * Constructs the exception with message. - */ - public UserAccountNotFoundException() { - super("User account not found"); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when user account does not exist with the given identifier or document number. + */ +public class UserAccountNotFoundException extends UserAccountException { + + /** + * Constructs the exception with message. + */ + public UserAccountNotFoundException() { + super("User account not found"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java index 62e99b77..d33f8e77 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when session status end result is ACCOUNT_UNUSABLE. - */ -public class UserAccountUnusableException extends UserAccountException { - - /** - * Constructs the exception with the default exception message. - */ - public UserAccountUnusableException() { - super("The account is currently unusable"); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when session status end result is ACCOUNT_UNUSABLE. + */ +public class UserAccountUnusableException extends UserAccountException { + + /** + * Constructs the exception with the default exception message. + */ + public UserAccountUnusableException() { + super("The account is currently unusable"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java index ce86589b..cdfe920d 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status end result is TIMEOUT. - */ -public class SessionTimeoutException extends UserActionException { - - /** - * Constructs the exception with default message. - */ - public SessionTimeoutException() { - super("Session timed out without getting any response from user"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status end result is TIMEOUT. + */ +public class SessionTimeoutException extends UserActionException { + + /** + * Constructs the exception with default message. + */ + public SessionTimeoutException() { + super("Session timed out without getting any response from user"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java index edea95a6..77912b04 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_CERT_CHOICE. - * This happens when user has multiple accounts and presses Cancel on device choice screen on any device. - */ -public class UserRefusedCertChoiceException extends UserRefusedException { - - /** - * Constructs a new UserRefusedDisplayTextAndPinException with the default exception message. - */ - public UserRefusedCertChoiceException() { - super("User has multiple accounts and pressed Cancel on device choice screen on any device."); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_CERT_CHOICE. + * This happens when user has multiple accounts and presses Cancel on device choice screen on any device. + */ +public class UserRefusedCertChoiceException extends UserRefusedException { + + /** + * Constructs a new UserRefusedDisplayTextAndPinException with the default exception message. + */ + public UserRefusedCertChoiceException() { + super("User has multiple accounts and pressed Cancel on device choice screen on any device."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java index 8444d5c3..13178fb2 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on confirmation message screen. - */ -public class UserRefusedConfirmationMessageException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedConfirmationMessageException() { - super("User cancelled on confirmationMessage screen"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation message screen. + */ +public class UserRefusedConfirmationMessageException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedConfirmationMessageException() { + super("User cancelled on confirmationMessage screen"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java index 06a3c5a4..aee41245 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on confirmation and verification code choice screen. - */ -public class UserRefusedConfirmationMessageWithVerificationChoiceException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedConfirmationMessageWithVerificationChoiceException() { - super("User cancelled on confirmationMessageAndVerificationCodeChoice screen"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation and verification code choice screen. + */ +public class UserRefusedConfirmationMessageWithVerificationChoiceException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedConfirmationMessageWithVerificationChoiceException() { + super("User cancelled on confirmationMessageAndVerificationCodeChoice screen"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java index 723d0a18..67b3ac8a 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on display text and PIN screen. - */ -public class UserRefusedDisplayTextAndPinException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedDisplayTextAndPinException() { - super("User pressed Cancel on PIN screen."); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on display text and PIN screen. + */ +public class UserRefusedDisplayTextAndPinException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedDisplayTextAndPinException() { + super("User pressed Cancel on PIN screen."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java index d729292d..7492111a 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java @@ -1,52 +1,52 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status end result is USER_REFUSED. - */ -public class UserRefusedException extends UserActionException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedException() { - super("User pressed cancel in app"); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserRefusedException(String message) { - super(message); - } - -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status end result is USER_REFUSED. + */ +public class UserRefusedException extends UserActionException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedException() { + super("User pressed cancel in app"); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserRefusedException(String message) { + super(message); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java index 24b4256b..380e794e 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status result is WRONG_VC. - * This happens when user selects wrong verification code in the app. - */ -public class UserSelectedWrongVerificationCodeException extends UserActionException { - - /** - * Constructs the exception with the default exception message. - */ - public UserSelectedWrongVerificationCodeException() { - super("User selected wrong verification code"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status result is WRONG_VC. + * This happens when user selects wrong verification code in the app. + */ +public class UserSelectedWrongVerificationCodeException extends UserActionException { + + /** + * Constructs the exception with the default exception message. + */ + public UserSelectedWrongVerificationCodeException() { + super("User selected wrong verification code"); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java index a19b587c..a0ac91a8 100644 --- a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java +++ b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java @@ -1,158 +1,158 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.nio.charset.Charset; - -import org.glassfish.jersey.message.MessageUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; -import jakarta.ws.rs.client.ClientResponseContext; -import jakarta.ws.rs.client.ClientResponseFilter; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.WriterInterceptor; -import jakarta.ws.rs.ext.WriterInterceptorContext; - -public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor { - - private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); - private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; - - @Override - public void filter(ClientRequestContext requestContext) { - if (logger.isDebugEnabled()) { - logUrl(requestContext); - } - if (logger.isTraceEnabled()) { - logHeaders(requestContext); - if (requestContext.hasEntity()) { - wrapEntityStreamWithLogger(requestContext); - } - } - } - - @Override - public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { - if (logger.isDebugEnabled()) { - logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); - } - if (logger.isTraceEnabled() && responseContext.hasEntity()) { - logResponseBody(responseContext); - } - } - - @Override - public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - context.proceed(); - if (logger.isTraceEnabled()) { - logRequestBody(context); - } - } - - private void logUrl(ClientRequestContext requestContext) { - String method = requestContext.getMethod(); - URI uri = requestContext.getUri(); - logger.debug(method + " " + uri.toString()); - } - - private void logHeaders(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getStringHeaders(); - if (headers != null) { - logger.trace("Request headers: {}", headers); - } - } - - private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { - OutputStream entityStream = requestContext.getEntityStream(); - LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); - requestContext.setEntityStream(loggingOutputStream); - requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); - } - - private void logResponseBody(ClientResponseContext responseContext) throws IOException { - Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); - InputStream entityStream = responseContext.getEntityStream(); - byte[] bodyBytes = readInputStreamBytes(entityStream); - responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); - logger.trace("Response body: " + new String(bodyBytes, charset)); - } - - private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = entityStream.read(buffer)) != -1) { - result.write(buffer, 0, length); - } - return result.toByteArray(); - } - - private void logRequestBody(WriterInterceptorContext context) { - LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); - if (loggingOutputStream != null) { - Charset charset = MessageUtils.getCharset(context.getMediaType()); - byte[] bytes = loggingOutputStream.getBytes(); - logger.trace("Message body: " + new String(bytes, charset)); - } - } - - public static class LoggingOutputStream extends FilterOutputStream { - - private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - public LoggingOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(byte[] b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } - - @Override - public void write(int b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } - - public byte[] getBytes() { - return byteArrayOutputStream.toByteArray(); - } - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; + +import org.glassfish.jersey.message.MessageUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; + + @Override + public void filter(ClientRequestContext requestContext) { + if (logger.isDebugEnabled()) { + logUrl(requestContext); + } + if (logger.isTraceEnabled()) { + logHeaders(requestContext); + if (requestContext.hasEntity()) { + wrapEntityStreamWithLogger(requestContext); + } + } + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); + } + if (logger.isTraceEnabled() && responseContext.hasEntity()) { + logResponseBody(responseContext); + } + } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + context.proceed(); + if (logger.isTraceEnabled()) { + logRequestBody(context); + } + } + + private void logUrl(ClientRequestContext requestContext) { + String method = requestContext.getMethod(); + URI uri = requestContext.getUri(); + logger.debug(method + " " + uri.toString()); + } + + private void logHeaders(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getStringHeaders(); + if (headers != null) { + logger.trace("Request headers: {}", headers); + } + } + + private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { + OutputStream entityStream = requestContext.getEntityStream(); + LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); + requestContext.setEntityStream(loggingOutputStream); + requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); + } + + private void logResponseBody(ClientResponseContext responseContext) throws IOException { + Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); + InputStream entityStream = responseContext.getEntityStream(); + byte[] bodyBytes = readInputStreamBytes(entityStream); + responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); + logger.trace("Response body: " + new String(bodyBytes, charset)); + } + + private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = entityStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toByteArray(); + } + + private void logRequestBody(WriterInterceptorContext context) { + LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); + if (loggingOutputStream != null) { + Charset charset = MessageUtils.getCharset(context.getMediaType()); + byte[] bytes = loggingOutputStream.getBytes(); + logger.trace("Message body: " + new String(bytes, charset)); + } + } + + public static class LoggingOutputStream extends FilterOutputStream { + + private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + public LoggingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } + + @Override + public void write(int b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } + + public byte[] getBytes() { + return byteArrayOutputStream.toByteArray(); + } + } +} diff --git a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java index 4a1c2fd3..a8994735 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java @@ -1,109 +1,109 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * Provides methods for querying sessions status and polling session status - */ -public class SessionStatusPoller { - - private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - - private final SmartIdConnector connector; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - - /** - * Constructs a new SessionStatusPoller with the specified SmartIdConnector. - * - * @param connector the SmartIdConnector to use for querying session status. - */ - public SessionStatusPoller(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Loops session status query until state is COMPLETE - * - * @param sessionId session id from init session response - * @return Sessions status - */ - public SessionStatus fetchFinalSessionStatus(String sessionId) { - logger.debug("Starting to poll session status for session {}", sessionId); - try { - return pollForFinalSessionStatus(sessionId); - } catch (InterruptedException ex) { - logger.error("Failed to poll session status", ex); - throw new SmartIdClientException("Failed to poll session status", ex); - } - } - - private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { - SessionStatus sessionStatus = null; - while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - sessionStatus = getSessionStatus(sessionId); - if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - break; - } - logger.debug("Sleeping for {} {}", pollingSleepTimeout, pollingSleepTimeUnit); - pollingSleepTimeUnit.sleep(pollingSleepTimeout); - } - logger.debug("Got final session status response"); - return sessionStatus; - } - - /** - * Query session status - * - * @param sessionId session id from init session response - * @return Sessions status - */ - public SessionStatus getSessionStatus(String sessionId) { - logger.debug("Querying session status"); - return connector.getSessionStatus(sessionId); - } - - /** - * Set polling sleep time - * - * @param unit time unit {@link TimeUnit} - * @param timeout time - */ - public void setPollingSleepTime(TimeUnit unit, long timeout) { - logger.debug("Setting polling sleep time to {} {}", timeout, unit); - this.pollingSleepTimeUnit = unit; - this.pollingSleepTimeout = timeout; - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * Provides methods for querying sessions status and polling session status + */ +public class SessionStatusPoller { + + private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); + + private final SmartIdConnector connector; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + + /** + * Constructs a new SessionStatusPoller with the specified SmartIdConnector. + * + * @param connector the SmartIdConnector to use for querying session status. + */ + public SessionStatusPoller(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Loops session status query until state is COMPLETE + * + * @param sessionId session id from init session response + * @return Sessions status + */ + public SessionStatus fetchFinalSessionStatus(String sessionId) { + logger.debug("Starting to poll session status for session {}", sessionId); + try { + return pollForFinalSessionStatus(sessionId); + } catch (InterruptedException ex) { + logger.error("Failed to poll session status", ex); + throw new SmartIdClientException("Failed to poll session status", ex); + } + } + + private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { + SessionStatus sessionStatus = null; + while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + sessionStatus = getSessionStatus(sessionId); + if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + break; + } + logger.debug("Sleeping for {} {}", pollingSleepTimeout, pollingSleepTimeUnit); + pollingSleepTimeUnit.sleep(pollingSleepTimeout); + } + logger.debug("Got final session status response"); + return sessionStatus; + } + + /** + * Query session status + * + * @param sessionId session id from init session response + * @return Sessions status + */ + public SessionStatus getSessionStatus(String sessionId) { + logger.debug("Querying session status"); + return connector.getSessionStatus(sessionId); + } + + /** + * Set polling sleep time + * + * @param unit time unit {@link TimeUnit} + * @param timeout time + */ + public void setPollingSleepTime(TimeUnit unit, long timeout) { + logger.debug("Setting polling sleep time to {} {}", timeout, unit); + this.pollingSleepTimeUnit = unit; + this.pollingSleepTimeout = timeout; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 37ad21a8..54f3d60a 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -1,197 +1,197 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * SmartIdConnector is the main interface for interacting with the Smart-ID service. - * It provides methods to initiate various types of sessions (authentication, signature, certificate choice) - * and to query session status and certificates. - */ -public interface SmartIdConnector extends Serializable { - - /** - * Get session status for the given session ID. - * - * @param sessionId The session ID - * @return The session status - * @throws SessionNotFoundException If the session is not found - */ - SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; - - /** - * Set the session status response socket open time - * - * @param sessionStatusResponseSocketOpenTimeUnit The time unit of the open time - * @param sessionStatusResponseSocketOpenTimeValue The value of the open time - */ - void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); - - /** - * Initiates a device link based certificate choice request. - * - * @param request CertificateChoiceSessionRequest containing necessary parameters - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request); - - /** - * Initiates a linked notification based signature session. - * - * @param request LinkedSignatureSessionRequest containing necessary parameters - * @param documentNumber The document number to be used for the session - * @return LinkedSignatureSessionResponse containing sessionID - */ - LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); - - /** - * Initiates a notification based certificate choice request. - * - * @param request CertificateChoiceSessionRequest containing necessary parameters - * @param semanticsIdentifier The semantics identifier to be used for the session - * @return NotificationCertificateChoiceSessionResponse containing sessionID - */ - NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Queries signing certificate by document number. - * - * @param request CertificateByDocumentNumberRequest containing necessary parameters - * @param documentNumber The document number - * @return CertificateResponse containing response state and certificate information. - */ - CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); - - /** - * Initiates a device link based signature sessions. - * - * @param request SignatureSessionRequest containing necessary parameters for the signature session - * @param semanticsIdentifier The semantics identifier - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Initiates a device link based signature sessions. - * - * @param request SignatureSessionRequest containing necessary parameters for the signature session - * @param documentNumber The document number - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber); - - /** - * Initiates a notification-based signature session using a semantics identifier. - * - * @param request The notification signature session request containing the required parameters. - * @param semanticsIdentifier The semantics identifier for the user initiating the session. - * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. - */ - NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Initiates a notification-based signature session using a document number. - * - * @param request The notification signature session request containing the required parameters. - * @param documentNumber The document number for the user initiating the session. - * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. - */ - NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber); - - /** - * Set the SSL context to use for secure communication - * - * @param sslContext The SSL context - */ - void setSslContext(SSLContext sslContext); - - /** - * Create anonymous authentication session with device link - * - * @param authenticationRequest The device link authentication session request - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest); - - /** - * Create authentication session with device link using semantics identifier - * - * @param authenticationRequest The device link authentication session request - * @param semanticsIdentifier The semantics identifier - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); - - /** - * Create authentication session with device link using document number - * - * @param authenticationRequest The device link authentication session request - * @param documentNumber The document number - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); - - /** - * Create authentication session with notification using semantics identifier - * - * @param authenticationRequest The notification authentication session request - * @param semanticsIdentifier The semantics identifier - * @return The notification authentication session response - */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); - - /** - * Create authentication session with notification using document number - * - * @param authenticationRequest The notification authentication session request - * @param documentNumber The document number - * @return The notification authentication session response - */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber); -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * SmartIdConnector is the main interface for interacting with the Smart-ID service. + * It provides methods to initiate various types of sessions (authentication, signature, certificate choice) + * and to query session status and certificates. + */ +public interface SmartIdConnector extends Serializable { + + /** + * Get session status for the given session ID. + * + * @param sessionId The session ID + * @return The session status + * @throws SessionNotFoundException If the session is not found + */ + SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; + + /** + * Set the session status response socket open time + * + * @param sessionStatusResponseSocketOpenTimeUnit The time unit of the open time + * @param sessionStatusResponseSocketOpenTimeValue The value of the open time + */ + void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); + + /** + * Initiates a device link based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request); + + /** + * Initiates a linked notification based signature session. + * + * @param request LinkedSignatureSessionRequest containing necessary parameters + * @param documentNumber The document number to be used for the session + * @return LinkedSignatureSessionResponse containing sessionID + */ + LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); + + /** + * Initiates a notification based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @param semanticsIdentifier The semantics identifier to be used for the session + * @return NotificationCertificateChoiceSessionResponse containing sessionID + */ + NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Queries signing certificate by document number. + * + * @param request CertificateByDocumentNumberRequest containing necessary parameters + * @param documentNumber The document number + * @return CertificateResponse containing response state and certificate information. + */ + CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); + + /** + * Initiates a device link based signature sessions. + * + * @param request SignatureSessionRequest containing necessary parameters for the signature session + * @param semanticsIdentifier The semantics identifier + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a device link based signature sessions. + * + * @param request SignatureSessionRequest containing necessary parameters for the signature session + * @param documentNumber The document number + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber); + + /** + * Initiates a notification-based signature session using a semantics identifier. + * + * @param request The notification signature session request containing the required parameters. + * @param semanticsIdentifier The semantics identifier for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a notification-based signature session using a document number. + * + * @param request The notification signature session request containing the required parameters. + * @param documentNumber The document number for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber); + + /** + * Set the SSL context to use for secure communication + * + * @param sslContext The SSL context + */ + void setSslContext(SSLContext sslContext); + + /** + * Create anonymous authentication session with device link + * + * @param authenticationRequest The device link authentication session request + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest); + + /** + * Create authentication session with device link using semantics identifier + * + * @param authenticationRequest The device link authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with device link using document number + * + * @param authenticationRequest The device link authentication session request + * @param documentNumber The document number + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); + + /** + * Create authentication session with notification using semantics identifier + * + * @param authenticationRequest The notification authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with notification using document number + * + * @param authenticationRequest The notification authentication session request + * @param documentNumber The document number + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber); +} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index af11880e..cf3e5bae 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -1,411 +1,411 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; - -import java.io.Serial; -import java.net.URI; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SessionStatusRequest; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ClientErrorException; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.ServerErrorException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Smart-ID REST connector implementation. - */ -public class SmartIdRestConnector implements SmartIdConnector { - - @Serial - private static final long serialVersionUID = 2025_09_10L; - - private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); - - private static final String SESSION_STATUS_URI = "/session/{sessionId}"; - - private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; - private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; - - private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "signature/certificate-choice/notification/etsi"; - - private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; - - private static final String DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/device-link/etsi"; - private static final String DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/device-link/document"; - - private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; - private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; - - private static final String ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH = "authentication/device-link/anonymous"; - private static final String DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/device-link/etsi"; - private static final String DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/device-link/document"; - - private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; - private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; - - private final String endpointUrl; - private transient Configuration clientConfig; - private transient Client configuredClient; - private transient SSLContext sslContext; - private long sessionStatusResponseSocketOpenTimeValue; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - - /** - * Creates a new instance of SmartIdRestConnector. - * - * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) - */ - public SmartIdRestConnector(String baseUrl) { - this.endpointUrl = baseUrl; - } - - /** - * Creates a new instance of SmartIdRestConnector with a pre-configured client. - * - * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) - * @param configuredClient a pre-configured client instace - */ - public SmartIdRestConnector(String baseUrl, Client configuredClient) { - this(baseUrl); - this.configuredClient = configuredClient; - } - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - logger.debug("Getting session status for sessionId: {}", sessionId); - SessionStatusRequest request = createSessionStatusRequest(sessionId); - UriBuilder uriBuilder = UriBuilder - .fromUri(endpointUrl) - .path(SESSION_STATUS_URI); - addResponseSocketOpenTimeUrlParameter(request, uriBuilder); - URI uri = uriBuilder.build(sessionId); - - try { - return prepareClient(uri).get(SessionStatus.class); - } catch (NotFoundException ex) { - logger.warn("Session {} not found: {}", request, ex.getMessage()); - throw new SessionNotFoundException(); - } - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - logger.debug("Starting device link authentication session with semantics identifier"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { - logger.debug("Starting device link authentication session with document number"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest) { - logger.debug("Starting anonymous device link authentication session"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); - } - - @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request) { - logger.debug("Initiating device link based certificate choice request"); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber) { - logger.debug("Starting linked notification-based signature session"); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, LinkedSignatureSessionResponse.class); - } - - @Override - public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); - } - - public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, CertificateResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, NotificationSignatureSessionResponse.class); - } - - @Override - public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, NotificationSignatureSessionResponse.class); - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } - - @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } - - /** - * Prepare client for the request. - * - * @param uri the target URI - * @return prepared invocation builder - */ - protected Invocation.Builder prepareClient(URI uri) { - Client client; - if (this.configuredClient == null) { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (null != this.clientConfig) { - clientBuilder.withConfig(this.clientConfig); - } - if (null != this.sslContext) { - clientBuilder.sslContext(this.sslContext); - } - client = clientBuilder.build(); - } else { - client = this.configuredClient; - } - - return client - .register(new LoggingFilter()) - .target(uri) - .request() - .accept(APPLICATION_JSON_TYPE) - .header("User-Agent", buildUserAgentString()); - } - - /** - * Build user-agent string. - * - * @return user-agent string in the format: smart-id-java-client/[client-version] (Java/[java-version]) - */ - protected String buildUserAgentString() { - return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; - } - - /** - * Get client version from package implementation version. - * - * @return client version or "-" - */ - protected String getClientVersion() { - String clientVersion = getClass().getPackage().getImplementationVersion(); - return clientVersion == null ? "-" : clientVersion; - } - - /** - * Get JDK major version. - * - * @return JDK major version or "-" - */ - protected String getJdkMajorVersion() { - try { - return System.getProperty("java.version").split("_")[0]; - } catch (Exception e) { - return "-"; - } - } - - private T postRequest(URI uri, V request, Class responseType) { - try { - Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); - return prepareClient(uri).post(requestEntity, responseType); - } catch (NotAuthorizedException ex) { - logger.warn("Request is unauthorized for URI {}", uri, ex); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, ex); - } catch (BadRequestException ex) { - logger.warn("Request is invalid for URI {}", uri, ex); - throw new SmartIdClientException("Server refused the request", ex); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } catch (ClientErrorException ex) { - if (ex.getResponse().getStatus() == 471) { - logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); - throw new NoSuitableAccountOfRequestedTypeFoundException(); - } - if (ex.getResponse().getStatus() == 472) { - logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", ex); - throw new PersonShouldViewSmartIdPortalException(); - } - if (ex.getResponse().getStatus() == 480) { - logger.warn("Client-side API is too old and not supported anymore"); - throw new SmartIdClientException("Client-side API is too old and not supported anymore"); - } - throw ex; - } catch (ServerErrorException ex) { - if (ex.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", ex); - throw new ServerMaintenanceException(); - } - throw ex; - } - } - - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - var request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; - } - - private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { - if (request.isResponseSocketOpenTimeSet()) { - TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); - long timeValue = request.getResponseSocketOpenTimeValue(); - long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); - uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); - } - } +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + +import java.io.Serial; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionStatusRequest; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Smart-ID REST connector implementation. + */ +public class SmartIdRestConnector implements SmartIdConnector { + + @Serial + private static final long serialVersionUID = 2025_09_10L; + + private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); + + private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + + private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; + private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; + + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "signature/certificate-choice/notification/etsi"; + + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; + + private static final String DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/device-link/etsi"; + private static final String DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/device-link/document"; + + private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; + private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; + + private static final String ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH = "authentication/device-link/anonymous"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/device-link/etsi"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/device-link/document"; + + private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; + private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; + + private final String endpointUrl; + private transient Configuration clientConfig; + private transient Client configuredClient; + private transient SSLContext sslContext; + private long sessionStatusResponseSocketOpenTimeValue; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + + /** + * Creates a new instance of SmartIdRestConnector. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + */ + public SmartIdRestConnector(String baseUrl) { + this.endpointUrl = baseUrl; + } + + /** + * Creates a new instance of SmartIdRestConnector with a pre-configured client. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + * @param configuredClient a pre-configured client instace + */ + public SmartIdRestConnector(String baseUrl, Client configuredClient) { + this(baseUrl); + this.configuredClient = configuredClient; + } + + @Override + public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { + logger.debug("Getting session status for sessionId: {}", sessionId); + SessionStatusRequest request = createSessionStatusRequest(sessionId); + UriBuilder uriBuilder = UriBuilder + .fromUri(endpointUrl) + .path(SESSION_STATUS_URI); + addResponseSocketOpenTimeUrlParameter(request, uriBuilder); + URI uri = uriBuilder.build(sessionId); + + try { + return prepareClient(uri).get(SessionStatus.class); + } catch (NotFoundException ex) { + logger.warn("Session {} not found: {}", request, ex.getMessage()); + throw new SessionNotFoundException(); + } + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + logger.debug("Starting device link authentication session with semantics identifier"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { + logger.debug("Starting device link authentication session with document number"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest) { + logger.debug("Starting anonymous device link authentication session"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request) { + logger.debug("Initiating device link based certificate choice request"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber) { + logger.debug("Starting linked notification-based signature session"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, LinkedSignatureSessionResponse.class); + } + + @Override + public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); + } + + public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, CertificateResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); + } + + @Override + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; + } + + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + + /** + * Prepare client for the request. + * + * @param uri the target URI + * @return prepared invocation builder + */ + protected Invocation.Builder prepareClient(URI uri) { + Client client; + if (this.configuredClient == null) { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (null != this.clientConfig) { + clientBuilder.withConfig(this.clientConfig); + } + if (null != this.sslContext) { + clientBuilder.sslContext(this.sslContext); + } + client = clientBuilder.build(); + } else { + client = this.configuredClient; + } + + return client + .register(new LoggingFilter()) + .target(uri) + .request() + .accept(APPLICATION_JSON_TYPE) + .header("User-Agent", buildUserAgentString()); + } + + /** + * Build user-agent string. + * + * @return user-agent string in the format: smart-id-java-client/[client-version] (Java/[java-version]) + */ + protected String buildUserAgentString() { + return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; + } + + /** + * Get client version from package implementation version. + * + * @return client version or "-" + */ + protected String getClientVersion() { + String clientVersion = getClass().getPackage().getImplementationVersion(); + return clientVersion == null ? "-" : clientVersion; + } + + /** + * Get JDK major version. + * + * @return JDK major version or "-" + */ + protected String getJdkMajorVersion() { + try { + return System.getProperty("java.version").split("_")[0]; + } catch (Exception e) { + return "-"; + } + } + + private T postRequest(URI uri, V request, Class responseType) { + try { + Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); + return prepareClient(uri).post(requestEntity, responseType); + } catch (NotAuthorizedException ex) { + logger.warn("Request is unauthorized for URI {}", uri, ex); + throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, ex); + } catch (BadRequestException ex) { + logger.warn("Request is invalid for URI {}", uri, ex); + throw new SmartIdClientException("Server refused the request", ex); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } catch (ClientErrorException ex) { + if (ex.getResponse().getStatus() == 471) { + logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); + throw new NoSuitableAccountOfRequestedTypeFoundException(); + } + if (ex.getResponse().getStatus() == 472) { + logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", ex); + throw new PersonShouldViewSmartIdPortalException(); + } + if (ex.getResponse().getStatus() == 480) { + logger.warn("Client-side API is too old and not supported anymore"); + throw new SmartIdClientException("Client-side API is too old and not supported anymore"); + } + throw ex; + } catch (ServerErrorException ex) { + if (ex.getResponse().getStatus() == 580) { + logger.warn("Server is under maintenance, retry later", ex); + throw new ServerMaintenanceException(); + } + throw ex; + } + } + + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + var request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; + } + + private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { + if (request.isResponseSocketOpenTimeSet()) { + TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); + long timeValue = request.getResponseSocketOpenTimeValue(); + long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); + uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); + } + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index b2bf44c4..0332f4b7 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * ACSP_V2 signature protocol parameters - * - * @param rpChallenge Required. The RP challenge in Base64 encoding - * @param signatureAlgorithm Required. The signature algorithm. Only supported value is rsassa-pss - * @param signatureAlgorithmParameters Required. The signature algorithm parameters - */ -public record AcspV2SignatureProtocolParameters(String rpChallenge, - String signatureAlgorithm, - SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * ACSP_V2 signature protocol parameters + * + * @param rpChallenge Required. The RP challenge in Base64 encoding + * @param signatureAlgorithm Required. The signature algorithm. Only supported value is rsassa-pss + * @param signatureAlgorithmParameters Required. The signature algorithm parameters + */ +public record AcspV2SignatureProtocolParameters(String rpChallenge, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java index 443d2177..bb94c73a 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request for querying certificate by document number. - * - * @param relyingPartyUUID Required. The relying party UUID - * @param relyingPartyName Required. The relying party name - * @param certificateLevel The certificate level. Possible values are "QSCD", "QUALIFIED" and "ADVANCED". If not specified, defaults to "QUALIFIED" - */ -public record CertificateByDocumentNumberRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request for querying certificate by document number. + * + * @param relyingPartyUUID Required. The relying party UUID + * @param relyingPartyName Required. The relying party name + * @param certificateLevel The certificate level. Possible values are "QSCD", "QUALIFIED" and "ADVANCED". If not specified, defaults to "QUALIFIED" + */ +public record CertificateByDocumentNumberRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java index 55b68605..369a8c9f 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Certificate info - * - * @param value Required. The certificate data in Base64-encoded format. - * @param certificateLevel Required. The certificate level, e.g. "QUALIFIED" or "ADVANCED". - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record CertificateInfo(String value, String certificateLevel) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Certificate info + * + * @param value Required. The certificate data in Base64-encoded format. + * @param certificateLevel Required. The certificate level, e.g. "QUALIFIED" or "ADVANCED". + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record CertificateInfo(String value, String certificateLevel) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java index 9bc32de6..fdce7dc2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Response of certificate queried by document number - * - * @param state Required. State of the certificate - * @param cert Required if state is OK. Certificate information {@link CertificateInfo} - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Response of certificate queried by document number + * + * @param state Required. State of the certificate + * @param cert Required if state is OK. Certificate information {@link CertificateInfo} + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java index abc502a4..0b61c316 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java @@ -1,57 +1,57 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.SignatureProtocol; - -/** - * Device link authentication session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for authentication. - * @param signatureProtocol Required. Authentication signature protocol to be used - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param interactions Required. Interaction to be used in the authentication session - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that the client could use - * @param initialCallbackUrl URL to which the user will be redirected. - */ -public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - SignatureProtocol signatureProtocol, - AcspV2SignatureProtocolParameters signatureProtocolParameters, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.SignatureProtocol; + +/** + * Device link authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Authentication signature protocol to be used + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + SignatureProtocol signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java index 0bfd03e5..01092a68 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request to create a Device Link session for choosing a certificate. - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param requestProperties Additional request properties - * @param initialCallbackUrl Initial callback URL to be used instead of the default one configured for the RP. - */ -public record DeviceLinkCertificateChoiceSessionRequest( - String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { - +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request to create a Device Link session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + * @param initialCallbackUrl Initial callback URL to be used instead of the default one configured for the RP. + */ +public record DeviceLinkCertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { + } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index 55b0f039..ef012808 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -1,71 +1,71 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.net.URI; -import java.time.Instant; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Response of session creation for device link flows - * - * @param sessionID Required. The unique identifier of the session. - * @param sessionToken Required. The token of the session. - * @param sessionSecret Required. The secret for the session. - * @param deviceLinkBase Required. Base URI for generating device link. - * @param receivedAt Timestamp when the response was received. - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record DeviceLinkSessionResponse(String sessionID, - String sessionToken, - String sessionSecret, - URI deviceLinkBase, - Instant receivedAt) implements Serializable { - - /** - * Initializes a new instance of the {@link DeviceLinkSessionResponse} class. - *

- * The receivedAt value is set to the current time. - * - * @param sessionID Required. The unique identifier of the session. - * @param sessionToken Required. The token of the session. - * @param sessionSecret Required. The secret for the session. - * @param deviceLinkBase Required. Base URI for generating device link - */ - @JsonCreator - public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, - @JsonProperty("sessionToken") String sessionToken, - @JsonProperty("sessionSecret") String sessionSecret, - @JsonProperty("deviceLinkBase") URI deviceLinkBase) { - this(sessionID, sessionToken, sessionSecret, deviceLinkBase, Instant.now()); - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.net.URI; +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response of session creation for device link flows + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link. + * @param receivedAt Timestamp when the response was received. + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record DeviceLinkSessionResponse(String sessionID, + String sessionToken, + String sessionSecret, + URI deviceLinkBase, + Instant receivedAt) implements Serializable { + + /** + * Initializes a new instance of the {@link DeviceLinkSessionResponse} class. + *

+ * The receivedAt value is set to the current time. + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link + */ + @JsonCreator + public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, + @JsonProperty("sessionToken") String sessionToken, + @JsonProperty("sessionSecret") String sessionSecret, + @JsonProperty("deviceLinkBase") URI deviceLinkBase) { + this(sessionID, sessionToken, sessionSecret, deviceLinkBase, Instant.now()); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java index e1f16fa2..7602b237 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java @@ -1,58 +1,58 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Device link-based signature session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for signing. - * @param signatureProtocol Required. Signature protocol to be used for signing. - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param interactions Required. Interaction to be used in the signature session - * @param requestProperties Additional properties for the request - * @param initialCallbackUrl URL to which the user will be redirected. - */ -public record DeviceLinkSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Device link-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index 2e84645d..5d71e43d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Interaction to be used in authentication and signing requests - * - * @param type Required. The interaction type - * @param displayText60 Requirement depends on the type. The text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 Requirement depends on the type. the text to be displayed on the device screen (maximum length 200 characters). - */ -public record Interaction(String type, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText60, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText200) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Interaction to be used in authentication and signing requests + * + * @param type Required. The interaction type + * @param displayText60 Requirement depends on the type. The text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 Requirement depends on the type. the text to be displayed on the device screen (maximum length 200 characters). + */ +public record Interaction(String type, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText60, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText200) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java index ebad4ab8..ce9131bd 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java @@ -1,57 +1,57 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Linked signature session request - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Certificate level. Possible values: QSCD, QUALIFIED, ADVANCED, - * @param signatureProtocol Required. Signature protocol. Only RAW_DIGEST_SIGNATURE is supported for signing. - * @param signatureProtocolParameters Required. RAW_DIGEST_SIGNATURE signature protocol parameters - * @param linkedSessionID Required. ID of the anonymous certificate choice session to be linked with this signature session. - * @param nonce Random value to cancel out idempotence of the request. - * @param interactions Required. Device link interactions should be used. - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - */ -public record LinkedSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - String linkedSessionID, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Linked signature session request + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Certificate level. Possible values: QSCD, QUALIFIED, ADVANCED, + * @param signatureProtocol Required. Signature protocol. Only RAW_DIGEST_SIGNATURE is supported for signing. + * @param signatureProtocolParameters Required. RAW_DIGEST_SIGNATURE signature protocol parameters + * @param linkedSessionID Required. ID of the anonymous certificate choice session to be linked with this signature session. + * @param nonce Random value to cancel out idempotence of the request. + * @param interactions Required. Device link interactions should be used. + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + */ +public record LinkedSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + String linkedSessionID, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java index eede94a4..3c3cecaa 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Response for linked notification based signature session initiation. - * - * @param sessionID The session ID - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record LinkedSignatureSessionResponse(String sessionID) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Response for linked notification based signature session initiation. + * + * @param sessionID The session ID + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record LinkedSignatureSessionResponse(String sessionID) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java index 316bf206..95678a50 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java @@ -1,56 +1,56 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Notification-based authentication session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for authentication. - * @param signatureProtocol Required. Signature protocol to be used for authentication - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param interactions Required. Interaction to be used in the authentication session - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that the client could use - * @param vcType Required. Verification code type to be used in the authentication session - */ -public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - AcspV2SignatureProtocolParameters signatureProtocolParameters, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - String vcType) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Signature protocol to be used for authentication + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param vcType Required. Verification code type to be used in the authentication session + */ +public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + String vcType) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java index 1cb13f47..c28b1e6b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based authentication session response - * - * @param sessionID the ID of the created authentication session - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationAuthenticationSessionResponse(String sessionID) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based authentication session response + * + * @param sessionID the ID of the created authentication session + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationAuthenticationSessionResponse(String sessionID) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java index a665abde..10946bd2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java @@ -1,52 +1,52 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request to create a notification-based session for choosing a certificate. - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param requestProperties Additional request properties - */ -public record NotificationCertificateChoiceSessionRequest( - String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request to create a notification-based session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + */ +public record NotificationCertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java index 77e943ea..ecbec0da 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -1,40 +1,40 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based certificate choice response - * - * @param sessionID Required. The ID of the created certificate choice session. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationCertificateChoiceSessionResponse(String sessionID) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based certificate choice response + * + * @param sessionID Required. The ID of the created certificate choice session. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationCertificateChoiceSessionResponse(String sessionID) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java index 7abdd7e9..c99ef0a5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java @@ -1,56 +1,56 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Notification-based signature session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for signing. - * @param signatureProtocol Required. Signature protocol to be used for signing. - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param interactions Required. Interaction to be used in the signature session - * @param requestProperties Additional properties for the request - */ -public record NotificationSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + */ +public record NotificationSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java index 9085d2d1..edd7e335 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based signature session request - * - * @param sessionID Required. The ID of the created signature session. - * @param vc Required. Verification code details - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationSignatureSessionResponse(String sessionID, VerificationCode vc) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based signature session request + * + * @param sessionID Required. The ID of the created signature session. + * @param vc Required. Verification code details + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationSignatureSessionResponse(String sessionID, VerificationCode vc) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index 16e9fe9b..616a3e99 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Parameters for protocol RAW_DIGEST_SIGNATURE - * - * @param digest Required. The digest to be signed, Base64 encoded. - * @param signatureAlgorithm Required. The signature algorithm. Supported value is RSASSA-PSS. - * @param signatureAlgorithmParameters Required. The parameters for signature algorithm. - */ -public record RawDigestSignatureProtocolParameters(String digest, - String signatureAlgorithm, - SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Parameters for protocol RAW_DIGEST_SIGNATURE + * + * @param digest Required. The digest to be signed, Base64 encoded. + * @param signatureAlgorithm Required. The signature algorithm. Supported value is RSASSA-PSS. + * @param signatureAlgorithmParameters Required. The parameters for signature algorithm. + */ +public record RawDigestSignatureProtocolParameters(String digest, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index c4832677..d46700f2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -1,39 +1,39 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Additional request properties - * - * @param shareMdClientIpAddress Set if the client's device IP address should be provided in sessions status response - */ -public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Additional request properties + * + * @param shareMdClientIpAddress Set if the client's device IP address should be provided in sessions status response + */ +public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java index ec8b1886..c8f3b9cc 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java @@ -1,138 +1,138 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Representation of Semantic Identifier. - */ -public class SemanticsIdentifier implements Serializable { - - private final String identifier; - - /** - * Constructs a new SemanticsIdentifier with the specified identity type, country code and identity number. - * - * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} - * @param countryCode the country code (e.g., EE, LT, LV). See {@link CountryCode} - * @param identityNumber the identity number - */ - public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { - this.identifier = "" + identityType + countryCode + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identity type, country code string and identity number. - * - * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} - * @param countryCodeString country code as string (e.g., EE, LT, LV) - * @param identityNumber the identity number - */ - public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { - this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identity type string, country code string and identity number. - * - * @param identityTypeString the identity type as string (e.g., PAS, IDC, PNO) - * @param countryCodeString country code as string (e.g., EE, LT, LV) - * @param identityNumber the identity number - */ - public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { - this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identifier string. - * - * @param identifier the full semantics identifier string (e.g., "PAS EE-1234567890") - */ - public SemanticsIdentifier(String identifier) { - this.identifier = identifier; - } - - /** - * Gets the full semantics identifier string. - * - * @return the full semantics identifier string - */ - public String getIdentifier() { - return identifier; - } - - /** - * 3-character identity type codes for SemanticsIdentifier - */ - public enum IdentityType { - - /** - * PAS - Passport - */ - PAS, - - /** - * IDC - Identity Card - */ - IDC, - - /** - * PNO - Personal Number - */ - PNO - } - - /** - * 2-character country codes for SemanticsIdentifier - */ - public enum CountryCode { - - /** - * Estonia - */ - EE, - - /** - * Lithuania - */ - LT, - - /** - * Latvia - */ - LV - } - - @Override - public String toString() { - return "SemanticsIdentifier{" + - "identifier='" + identifier + '\'' + - '}'; - } - -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Representation of Semantic Identifier. + */ +public class SemanticsIdentifier implements Serializable { + + private final String identifier; + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCode the country code (e.g., EE, LT, LV). See {@link CountryCode} + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { + this.identifier = "" + identityType + countryCode + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code string and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { + this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type string, country code string and identity number. + * + * @param identityTypeString the identity type as string (e.g., PAS, IDC, PNO) + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { + this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identifier string. + * + * @param identifier the full semantics identifier string (e.g., "PAS EE-1234567890") + */ + public SemanticsIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Gets the full semantics identifier string. + * + * @return the full semantics identifier string + */ + public String getIdentifier() { + return identifier; + } + + /** + * 3-character identity type codes for SemanticsIdentifier + */ + public enum IdentityType { + + /** + * PAS - Passport + */ + PAS, + + /** + * IDC - Identity Card + */ + IDC, + + /** + * PNO - Personal Number + */ + PNO + } + + /** + * 2-character country codes for SemanticsIdentifier + */ + public enum CountryCode { + + /** + * Estonia + */ + EE, + + /** + * Lithuania + */ + LT, + + /** + * Latvia + */ + LV + } + + @Override + public String toString() { + return "SemanticsIdentifier{" + + "identifier='" + identifier + '\'' + + '}'; + } + +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java index 1a3accf1..ec4434e1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java @@ -1,80 +1,80 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Certificate data in session status response. - *

- * value - the certificate data in Base64-encoded format - * certificateLevel - the certificate level. Possible values: QUALIFIED or ADVANCED - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionCertificate implements Serializable { - - private String value; - private String certificateLevel; - - /** - * Get the certificate value. - * - * @return the certificate data in Base64-encoded format - */ - public String getValue() { - return value; - } - - /** - * Set the certificate value. - * - * @param value the certificate data in Base64-encoded format - */ - public void setValue(String value) { - this.value = value; - } - - /** - * Gets the certificate level. - * - * @return the certificate level - */ - public String getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - */ - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Certificate data in session status response. + *

+ * value - the certificate data in Base64-encoded format + * certificateLevel - the certificate level. Possible values: QUALIFIED or ADVANCED + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionCertificate implements Serializable { + + private String value; + private String certificateLevel; + + /** + * Get the certificate value. + * + * @return the certificate data in Base64-encoded format + */ + public String getValue() { + return value; + } + + /** + * Set the certificate value. + * + * @param value the certificate data in Base64-encoded format + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the certificate level. + * + * @return the certificate level + */ + public String getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + */ + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java index 241378f5..e1844752 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java @@ -1,80 +1,80 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Mask generation algorithm data in session status response. - *

- * algorithm - Required. The algorithm name, e.g. "id-mgf1" - * parameters - Required. The mask generation algorithm parameters - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionMaskGenAlgorithm implements Serializable { - - private String algorithm; - private SessionMaskGenAlgorithmParameters parameters; - - /** - * Gets the algorithm. - * - * @return the algorithm - */ - public String getAlgorithm() { - return algorithm; - } - - /** - * Sets the algorithm. - * - * @param algorithm the algorithm - */ - public void setAlgorithm(String algorithm) { - this.algorithm = algorithm; - } - - /** - * Gets the parameters. - * - * @return the parameters - */ - public SessionMaskGenAlgorithmParameters getParameters() { - return parameters; - } - - /** - * Sets the parameters. - * - * @param parameters the parameters - */ - public void setParameters(SessionMaskGenAlgorithmParameters parameters) { - this.parameters = parameters; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Mask generation algorithm data in session status response. + *

+ * algorithm - Required. The algorithm name, e.g. "id-mgf1" + * parameters - Required. The mask generation algorithm parameters + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithm implements Serializable { + + private String algorithm; + private SessionMaskGenAlgorithmParameters parameters; + + /** + * Gets the algorithm. + * + * @return the algorithm + */ + public String getAlgorithm() { + return algorithm; + } + + /** + * Sets the algorithm. + * + * @param algorithm the algorithm + */ + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + /** + * Gets the parameters. + * + * @return the parameters + */ + public SessionMaskGenAlgorithmParameters getParameters() { + return parameters; + } + + /** + * Sets the parameters. + * + * @param parameters the parameters + */ + public void setParameters(SessionMaskGenAlgorithmParameters parameters) { + this.parameters = parameters; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java index 029782ba..1096caaf 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java @@ -1,60 +1,60 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Mask generation algorithm parameters. - *

- * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionMaskGenAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - - /** - * Gets hash algorithm. - * - * @return hash algorithm - */ - public String getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Sets hash algorithm. - * - * @param hashAlgorithm hash algorithm - */ - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Mask generation algorithm parameters. + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ + public String getHashAlgorithm() { + return hashAlgorithm; + } + + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java index f7c2ca6b..37e23c37 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java @@ -1,101 +1,101 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents how session ended - successfully, cancelled by user, timed out, etc. - * Available when session state is COMPLETE. - *

- * endResult - Required. Reason for the session state being COMPLETED. - * documentNumber - Required. User's document number - * details - Additional details if user refused interaction. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionResult implements Serializable { - - private String endResult; - private String documentNumber; - private SessionResultDetails details; - - /** - * Get exact end result of the session. - * - * @return end result of the session - */ - public String getEndResult() { - return endResult; - } - - /** - * Set end result of the session - * - * @param endResult end result of the session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Get document number of the user used in the session. - * - * @return document number of the user - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Set document number of the user - * - * @param documentNumber document number of the user - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - /** - * Get additional details - * - * @return details of the session result - */ - public SessionResultDetails getDetails() { - return details; - } - - /** - * Set details of the session result - * - * @param details details of the session result - */ - public void setDetails(SessionResultDetails details) { - this.details = details; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents how session ended - successfully, cancelled by user, timed out, etc. + * Available when session state is COMPLETE. + *

+ * endResult - Required. Reason for the session state being COMPLETED. + * documentNumber - Required. User's document number + * details - Additional details if user refused interaction. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResult implements Serializable { + + private String endResult; + private String documentNumber; + private SessionResultDetails details; + + /** + * Get exact end result of the session. + * + * @return end result of the session + */ + public String getEndResult() { + return endResult; + } + + /** + * Set end result of the session + * + * @param endResult end result of the session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Get document number of the user used in the session. + * + * @return document number of the user + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Set document number of the user + * + * @param documentNumber document number of the user + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + /** + * Get additional details + * + * @return details of the session result + */ + public SessionResultDetails getDetails() { + return details; + } + + /** + * Set details of the session result + * + * @param details details of the session result + */ + public void setDetails(SessionResultDetails details) { + this.details = details; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java index 26e82578..56a0f08c 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents additional info when end result if user refused interactions. - *

- * Required when end result is USER_REFUSED_INTERACTION. - *

- * interaction - Type of the interaction that was cancelled by the user, e.g. "displayTextAndPIN" - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionResultDetails implements Serializable { - - private String interaction; - - /** - * Gets type of the interaction that was cancelled by the user. - * - * @return type of the interaction that was cancelled by the user. - */ - public String getInteraction() { - return interaction; - } - - /** - * Sets type of the interaction type - * - * @param interaction type of the interaction - */ - public void setInteraction(String interaction) { - this.interaction = interaction; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents additional info when end result if user refused interactions. + *

+ * Required when end result is USER_REFUSED_INTERACTION. + *

+ * interaction - Type of the interaction that was cancelled by the user, e.g. "displayTextAndPIN" + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResultDetails implements Serializable { + + private String interaction; + + /** + * Gets type of the interaction that was cancelled by the user. + * + * @return type of the interaction that was cancelled by the user. + */ + public String getInteraction() { + return interaction; + } + + /** + * Sets type of the interaction type + * + * @param interaction type of the interaction + */ + public void setInteraction(String interaction) { + this.interaction = interaction; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java index 8bd48aad..a2c634cb 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java @@ -1,160 +1,160 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Signature data. - *

- * value - Required. Signature value in Base64-encoded format. - * serverRandom - Required. Server random value in Base64-encoded format. - * userChallenge - User challenge value in URL-safe Base64-encoded format. - * flowType - Required. The flow type, e.g. "QR", "Web2App". - * signatureAlgorithm - Required. The signature algorithm, e.g. "rsassa-pss". - * signatureAlgorithmParameters - Required. The signature algorithm parameters. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionSignature implements Serializable { - - private String value; - private String serverRandom; - private String userChallenge; - private String flowType; - private String signatureAlgorithm; - private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; - - /** - * Get the signature value. - * - * @return the signature value - */ - public String getValue() { - return value; - } - - /** - * Set the signature value. - * - * @param value the signature value - */ - public void setValue(String value) { - this.value = value; - } - - /** - * Get the server random value. - * - * @return the server random value - */ - public String getServerRandom() { - return serverRandom; - } - - /** - * Set the server random value. - * - * @param serverRandom the server random value - */ - public void setServerRandom(String serverRandom) { - this.serverRandom = serverRandom; - } - - /** - * Get the user challenge value. - * - * @return the user challenge value - */ - public String getUserChallenge() { - return userChallenge; - } - - /** - * Set the user challenge value. - * - * @param userChallenge the user challenge value - */ - public void setUserChallenge(String userChallenge) { - this.userChallenge = userChallenge; - } - - /** - * Get the flow type. - * - * @return the flow type - */ - public String getFlowType() { - return flowType; - } - - /** - * Set the flow type. - * - * @param flowType the flow type - */ - public void setFlowType(String flowType) { - this.flowType = flowType; - } - - /** - * Get the signature algorithm. - * - * @return the signature algorithm - */ - public String getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Set the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - */ - public void setSignatureAlgorithm(String signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - /** - * Get the signature algorithm parameters. - * - * @return the signature algorithm parameters - */ - public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { - return signatureAlgorithmParameters; - } - - /** - * Set the signature algorithm parameters. - * - * @param signatureAlgorithmParameters the signature algorithm parameters - */ - public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { - this.signatureAlgorithmParameters = signatureAlgorithmParameters; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Signature data. + *

+ * value - Required. Signature value in Base64-encoded format. + * serverRandom - Required. Server random value in Base64-encoded format. + * userChallenge - User challenge value in URL-safe Base64-encoded format. + * flowType - Required. The flow type, e.g. "QR", "Web2App". + * signatureAlgorithm - Required. The signature algorithm, e.g. "rsassa-pss". + * signatureAlgorithmParameters - Required. The signature algorithm parameters. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignature implements Serializable { + + private String value; + private String serverRandom; + private String userChallenge; + private String flowType; + private String signatureAlgorithm; + private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; + + /** + * Get the signature value. + * + * @return the signature value + */ + public String getValue() { + return value; + } + + /** + * Set the signature value. + * + * @param value the signature value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Get the server random value. + * + * @return the server random value + */ + public String getServerRandom() { + return serverRandom; + } + + /** + * Set the server random value. + * + * @param serverRandom the server random value + */ + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } + + /** + * Get the user challenge value. + * + * @return the user challenge value + */ + public String getUserChallenge() { + return userChallenge; + } + + /** + * Set the user challenge value. + * + * @param userChallenge the user challenge value + */ + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + /** + * Get the flow type. + * + * @return the flow type + */ + public String getFlowType() { + return flowType; + } + + /** + * Set the flow type. + * + * @param flowType the flow type + */ + public void setFlowType(String flowType) { + this.flowType = flowType; + } + + /** + * Get the signature algorithm. + * + * @return the signature algorithm + */ + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Set the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + */ + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Get the signature algorithm parameters. + * + * @return the signature algorithm parameters + */ + public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + /** + * Set the signature algorithm parameters. + * + * @param signatureAlgorithmParameters the signature algorithm parameters + */ + public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java index 5fbcee83..2f008632 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java @@ -1,120 +1,120 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Signature algorithm parameters - *

- * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" - * maskGenAlgorithm - Required. The mask generation algorithm - * saltLength - Required. The salt length, e.g. 32 for SHA-256 - * trailerField - Required. The trailer field, e.g. "0xbc"> - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionSignatureAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - private SessionMaskGenAlgorithm maskGenAlgorithm; - private Integer saltLength; - private String trailerField; - - /** - * Gets hash algorithm. - * - * @return hash algorithm - */ - public String getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Sets hash algorithm. - * - * @param hashAlgorithm hash algorithm - */ - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Gets mask generation algorithm. - * - * @return mask generation algorithm - */ - public SessionMaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - /** - * Sets mask generation algorithm. - * - * @param maskGenAlgorithm mask generation algorithm - */ - public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - /** - * Gets salt length. - * - * @return salt length - */ - public Integer getSaltLength() { - return saltLength; - } - - /** - * Sets salt length. - * - * @param saltLength salt length - */ - public void setSaltLength(Integer saltLength) { - this.saltLength = saltLength; - } - - /** - * Gets trailer field. - * - * @return trailer field - */ - public String getTrailerField() { - return trailerField; - } - - /** - * Sets trailer field. - * - * @param trailerField trailer field - */ - public void setTrailerField(String trailerField) { - this.trailerField = trailerField; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Signature algorithm parameters + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + * maskGenAlgorithm - Required. The mask generation algorithm + * saltLength - Required. The salt length, e.g. 32 for SHA-256 + * trailerField - Required. The trailer field, e.g. "0xbc"> + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignatureAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + private SessionMaskGenAlgorithm maskGenAlgorithm; + private Integer saltLength; + private String trailerField; + + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ + public String getHashAlgorithm() { + return hashAlgorithm; + } + + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + /** + * Gets mask generation algorithm. + * + * @return mask generation algorithm + */ + public SessionMaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + /** + * Sets mask generation algorithm. + * + * @param maskGenAlgorithm mask generation algorithm + */ + public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + /** + * Gets salt length. + * + * @return salt length + */ + public Integer getSaltLength() { + return saltLength; + } + + /** + * Sets salt length. + * + * @param saltLength salt length + */ + public void setSaltLength(Integer saltLength) { + this.saltLength = saltLength; + } + + /** + * Gets trailer field. + * + * @return trailer field + */ + public String getTrailerField() { + return trailerField; + } + + /** + * Sets trailer field. + * + * @param trailerField trailer field + */ + public void setTrailerField(String trailerField) { + this.trailerField = trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 59641acc..88738e06 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -1,200 +1,200 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents response for active session query. - *

- * state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE"> - * result - Required if state is "COMPLETE". Details about how session ended. - * signatureProtocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE". - * signature - Required if end result is OK. Signature data containing the actual signature and related information. - * cert - Required if end result is OK. Signer's certificate data. - * ignoredProperties - properties that were ignored from the session request. - * interactionTypeUsed - Required if end result is OK. Interaction type that was used in the session. - * deviceIpAddress - IP address of the device used in the session. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionStatus implements Serializable { - - private String state; - private SessionResult result; - private String signatureProtocol; - private SessionSignature signature; - private SessionCertificate cert; - private String[] ignoredProperties; - private String interactionTypeUsed; - private String deviceIpAddress; - - /** - * Get state of the session - * - * @return state of the session - */ - public String getState() { - return state; - } - - /** - * Set state of the session - * - * @param state state of the session - */ - public void setState(String state) { - this.state = state; - } - - /** - * Get result of the session - * - * @return result of the session - */ - public SessionResult getResult() { - return result; - } - - /** - * Set result of the session - * - * @param result result of the session - */ - public void setResult(SessionResult result) { - this.result = result; - } - - /** - * Get signature protocol used - * - * @return signature protocol used - */ - public String getSignatureProtocol() { - return signatureProtocol; - } - - /** - * Sets the signature protocol used - * - * @param signatureProtocol signature protocol used - */ - public void setSignatureProtocol(String signatureProtocol) { - this.signatureProtocol = signatureProtocol; - } - - /** - * Get signature of the session - * - * @return signature of the session - */ - public SessionSignature getSignature() { - return signature; - } - - /** - * Set signature of the session - * - * @param signature signature of the session - */ - public void setSignature(SessionSignature signature) { - this.signature = signature; - } - - /** - * Get certificate of the session - * - * @return certificate of the session - */ - public SessionCertificate getCert() { - return cert; - } - - /** - * Set certificate of the session - * - * @param cert certificate of the session - */ - public void setCert(SessionCertificate cert) { - this.cert = cert; - } - - /** - * Get ignored properties provided in the session request. - * - * @return ignored properties - */ - public String[] getIgnoredProperties() { - return ignoredProperties; - } - - /** - * Set ignored properties provided in the session request. - * - * @param ignoredProperties ignored properties - */ - public void setIgnoredProperties(String[] ignoredProperties) { - this.ignoredProperties = ignoredProperties; - } - - /** - * Gets the interaction type used in the session - * - * @return the interaction type used in session - */ - public String getInteractionTypeUsed() { - return interactionTypeUsed; - } - - /** - * Sets the interaction type used in the session - * - * @param interactionTypeUsed the interaction type used in session - */ - public void setInteractionTypeUsed(String interactionTypeUsed) { - this.interactionTypeUsed = interactionTypeUsed; - } - - /** - * Gets the IP address of the device used in the session - * - * @return the device IP address - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in the session - * - * @param deviceIpAddress the device IP address - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents response for active session query. + *

+ * state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE"> + * result - Required if state is "COMPLETE". Details about how session ended. + * signatureProtocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE". + * signature - Required if end result is OK. Signature data containing the actual signature and related information. + * cert - Required if end result is OK. Signer's certificate data. + * ignoredProperties - properties that were ignored from the session request. + * interactionTypeUsed - Required if end result is OK. Interaction type that was used in the session. + * deviceIpAddress - IP address of the device used in the session. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionStatus implements Serializable { + + private String state; + private SessionResult result; + private String signatureProtocol; + private SessionSignature signature; + private SessionCertificate cert; + private String[] ignoredProperties; + private String interactionTypeUsed; + private String deviceIpAddress; + + /** + * Get state of the session + * + * @return state of the session + */ + public String getState() { + return state; + } + + /** + * Set state of the session + * + * @param state state of the session + */ + public void setState(String state) { + this.state = state; + } + + /** + * Get result of the session + * + * @return result of the session + */ + public SessionResult getResult() { + return result; + } + + /** + * Set result of the session + * + * @param result result of the session + */ + public void setResult(SessionResult result) { + this.result = result; + } + + /** + * Get signature protocol used + * + * @return signature protocol used + */ + public String getSignatureProtocol() { + return signatureProtocol; + } + + /** + * Sets the signature protocol used + * + * @param signatureProtocol signature protocol used + */ + public void setSignatureProtocol(String signatureProtocol) { + this.signatureProtocol = signatureProtocol; + } + + /** + * Get signature of the session + * + * @return signature of the session + */ + public SessionSignature getSignature() { + return signature; + } + + /** + * Set signature of the session + * + * @param signature signature of the session + */ + public void setSignature(SessionSignature signature) { + this.signature = signature; + } + + /** + * Get certificate of the session + * + * @return certificate of the session + */ + public SessionCertificate getCert() { + return cert; + } + + /** + * Set certificate of the session + * + * @param cert certificate of the session + */ + public void setCert(SessionCertificate cert) { + this.cert = cert; + } + + /** + * Get ignored properties provided in the session request. + * + * @return ignored properties + */ + public String[] getIgnoredProperties() { + return ignoredProperties; + } + + /** + * Set ignored properties provided in the session request. + * + * @param ignoredProperties ignored properties + */ + public void setIgnoredProperties(String[] ignoredProperties) { + this.ignoredProperties = ignoredProperties; + } + + /** + * Gets the interaction type used in the session + * + * @return the interaction type used in session + */ + public String getInteractionTypeUsed() { + return interactionTypeUsed; + } + + /** + * Sets the interaction type used in the session + * + * @param interactionTypeUsed the interaction type used in session + */ + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; + } + + /** + * Gets the IP address of the device used in the session + * + * @return the device IP address + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in the session + * + * @param deviceIpAddress the device IP address + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java index faa46c19..1b2677bf 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java @@ -1,105 +1,105 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -/** - * Represents request to query session status. - *

- * sessionId - the session ID to query status for. - * responseSocketOpenTimeUnit - time unit of how much time a network request socket should be kept open. - * responseSocketOpenTimeValue - time value of how much time a network request socket should be kept opn. - */ -public class SessionStatusRequest implements Serializable { - - private final String sessionId; - private TimeUnit responseSocketOpenTimeUnit; - private long responseSocketOpenTimeValue; - - /** - * Constructs a new SessionStatusRequest with the specified session ID. - * - * @param sessionId the session ID to query status for. - */ - public SessionStatusRequest(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the session ID. - * - * @return the session ID. - */ - public String getSessionId() { - return sessionId; - } - - /** - * Request long poll timeout value. If not provided, a default is used. - *

- * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires - * set by this parameter. - *

- * Caller can tune the request parameters inside the bounds set by service operator. - * - * @param timeUnit time unit of how much time a network request socket should be kept open. - * @param timeValue time value of how much time a network request socket should be kept open. - */ - public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - responseSocketOpenTimeUnit = timeUnit; - responseSocketOpenTimeValue = timeValue; - } - - /** - * Gets whether response socket open time is set. - * - * @return true if response socket open time is set, false otherwise. - */ - public boolean isResponseSocketOpenTimeSet() { - return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; - } - - /** - * Gets response socket open time unit. - * - * @return response socket open time unit. - */ - public TimeUnit getResponseSocketOpenTimeUnit() { - return responseSocketOpenTimeUnit; - } - - /** - * Gets response socket open time value. - * - * @return response socket open time value. - */ - public long getResponseSocketOpenTimeValue() { - return responseSocketOpenTimeValue; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +/** + * Represents request to query session status. + *

+ * sessionId - the session ID to query status for. + * responseSocketOpenTimeUnit - time unit of how much time a network request socket should be kept open. + * responseSocketOpenTimeValue - time value of how much time a network request socket should be kept opn. + */ +public class SessionStatusRequest implements Serializable { + + private final String sessionId; + private TimeUnit responseSocketOpenTimeUnit; + private long responseSocketOpenTimeValue; + + /** + * Constructs a new SessionStatusRequest with the specified session ID. + * + * @param sessionId the session ID to query status for. + */ + public SessionStatusRequest(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Gets the session ID. + * + * @return the session ID. + */ + public String getSessionId() { + return sessionId; + } + + /** + * Request long poll timeout value. If not provided, a default is used. + *

+ * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires + * set by this parameter. + *

+ * Caller can tune the request parameters inside the bounds set by service operator. + * + * @param timeUnit time unit of how much time a network request socket should be kept open. + * @param timeValue time value of how much time a network request socket should be kept open. + */ + public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + responseSocketOpenTimeUnit = timeUnit; + responseSocketOpenTimeValue = timeValue; + } + + /** + * Gets whether response socket open time is set. + * + * @return true if response socket open time is set, false otherwise. + */ + public boolean isResponseSocketOpenTimeSet() { + return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; + } + + /** + * Gets response socket open time unit. + * + * @return response socket open time unit. + */ + public TimeUnit getResponseSocketOpenTimeUnit() { + return responseSocketOpenTimeUnit; + } + + /** + * Gets response socket open time value. + * + * @return response socket open time value. + */ + public long getResponseSocketOpenTimeValue() { + return responseSocketOpenTimeValue; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index 97ce547e..5fb57ff6 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Parameters for signature algorithm - * - * @param hashAlgorithm Required. The hash algorithm. - * Supported values are SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 - */ -public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Parameters for signature algorithm + * + * @param hashAlgorithm Required. The hash algorithm. + * Supported values are SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 + */ +public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index db9ff91d..c066e873 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Verification code details - * - * @param type Required. Verification code type - * @param value Required. Verification code value - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record VerificationCode(String type, String value) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Verification code details + * + * @param type Required. Verification code type + * @param value Required. Verification code value + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record VerificationCode(String type, String value) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java index b12e1fc2..a93ee3e0 100644 --- a/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java +++ b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java @@ -1,91 +1,91 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; -import ee.sk.smartid.exception.SessionSecretMismatchException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Utility class for callback URL query parameter related operations. - */ -public final class CallbackUrlUtil { - - private CallbackUrlUtil() { - } - - /** - * Creates a callback URL by appending a random URL-safe token as a query parameter to the provided base URL. - * - * @param baseUrl the URL to which the token will be appended as a query parameter - * @return a {@link CallbackUrl} containing the full callback URL and the generated token - */ - public static CallbackUrl createCallbackUrl(String baseUrl) { - if (StringUtil.isEmpty(baseUrl)) { - throw new SmartIdClientException("Parameter for 'baseUrl' cannot be empty"); - } - String urlToken = UrlSafeTokenGenerator.random(); - return new CallbackUrl(UriBuilder.fromUri(baseUrl).queryParam("value", urlToken).build(), urlToken); - } - - /** - * Validates that the session secret digest from the callback URL matches the calculated digest of the provided session secret. - * - * @param sessionSecretDigest the session secret digest received in the callback URL - * @param sessionSecret the original session secret from the session initialization response - * @throws SmartIdClientException when any input parameters are empty - * @throws SessionSecretMismatchException when the session secrets do not match - */ - public static void validateSessionSecretDigest(String sessionSecretDigest, String sessionSecret) { - if (StringUtil.isEmpty(sessionSecretDigest)) { - throw new SmartIdClientException("Parameter for 'sessionSecretDigest' cannot be empty"); - } - if (StringUtil.isEmpty(sessionSecret)) { - throw new SmartIdClientException("Parameter for 'sessionSecret' cannot be empty"); - } - String calculatedSessionSecret = calculateDigest(sessionSecret); - if (!sessionSecretDigest.equals(calculatedSessionSecret)) { - throw new SessionSecretMismatchException("Session secret digest from callback does not match calculated session secret digest"); - } - } - - private static String calculateDigest(String sessionSecret) { - try { - byte[] decodedSessionSecret = Base64.getDecoder().decode(sessionSecret); - byte[] sessionSecretDigest = DigestCalculator.calculateDigest(decodedSessionSecret, HashAlgorithm.SHA_256); - return Base64.getUrlEncoder().withoutPadding().encodeToString(sessionSecretDigest); - } catch (IllegalArgumentException ex) { - throw new SmartIdClientException("Parameter 'sessionSecret' is not Base64-encoded value", ex); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Utility class for callback URL query parameter related operations. + */ +public final class CallbackUrlUtil { + + private CallbackUrlUtil() { + } + + /** + * Creates a callback URL by appending a random URL-safe token as a query parameter to the provided base URL. + * + * @param baseUrl the URL to which the token will be appended as a query parameter + * @return a {@link CallbackUrl} containing the full callback URL and the generated token + */ + public static CallbackUrl createCallbackUrl(String baseUrl) { + if (StringUtil.isEmpty(baseUrl)) { + throw new SmartIdClientException("Parameter for 'baseUrl' cannot be empty"); + } + String urlToken = UrlSafeTokenGenerator.random(); + return new CallbackUrl(UriBuilder.fromUri(baseUrl).queryParam("value", urlToken).build(), urlToken); + } + + /** + * Validates that the session secret digest from the callback URL matches the calculated digest of the provided session secret. + * + * @param sessionSecretDigest the session secret digest received in the callback URL + * @param sessionSecret the original session secret from the session initialization response + * @throws SmartIdClientException when any input parameters are empty + * @throws SessionSecretMismatchException when the session secrets do not match + */ + public static void validateSessionSecretDigest(String sessionSecretDigest, String sessionSecret) { + if (StringUtil.isEmpty(sessionSecretDigest)) { + throw new SmartIdClientException("Parameter for 'sessionSecretDigest' cannot be empty"); + } + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter for 'sessionSecret' cannot be empty"); + } + String calculatedSessionSecret = calculateDigest(sessionSecret); + if (!sessionSecretDigest.equals(calculatedSessionSecret)) { + throw new SessionSecretMismatchException("Session secret digest from callback does not match calculated session secret digest"); + } + } + + private static String calculateDigest(String sessionSecret) { + try { + byte[] decodedSessionSecret = Base64.getDecoder().decode(sessionSecret); + byte[] sessionSecretDigest = DigestCalculator.calculateDigest(decodedSessionSecret, HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(sessionSecretDigest); + } catch (IllegalArgumentException ex) { + throw new SmartIdClientException("Parameter 'sessionSecret' is not Base64-encoded value", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index c946a88c..f1fcf869 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -1,203 +1,203 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.text.ParseException; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.ASN1GeneralizedTime; -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.DLSet; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for extracting attributes from X.509 certificates. - */ -public final class CertificateAttributeUtil { - - private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); - - private static final String CERTIFICATE_POLICY_OID = "2.5.29.32"; - private static final int KEY_USAGE_NON_REPUDIATION_INDEX = 1; - - private CertificateAttributeUtil() { - } - - /** - * Get Date-of-birth (DoB) from a specific certificate header (if present). - *

- * NB! This attribute may be present on some newer certificates (since ~ May 2021) but not all. - * - * @param x509Certificate Certificate to read the date-of-birth attribute from - * @return Person date of birth or null if this attribute is not set. - * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. - */ - public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { - Optional dateOfBirth = getDateOfBirthCertificateAttribute(x509Certificate); - - return dateOfBirth.map(date -> date.toInstant().atZone(ZoneOffset.UTC).toLocalDate()).orElse(null); - } - - /** - * Get value of attribute in X.500 principal. - * - * @param distinguishedName X.500 distinguished name using the format defined in RFC 2253. - * @param oid Object Identifier (OID) of the attribute to extract - * @return Attribute value - */ - public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { - var x500name = new X500Name(distinguishedName); - RDN[] rdns = x500name.getRDNs(oid); - if (rdns.length == 0) { - return Optional.empty(); - } - return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); - } - - /** - * Extracts certificate policy OID from the given X.509 certificate. - * - * @param certificate the X.509 certificate from which to extract the policy OIDs - * @return a set of certificate policy OIDs as strings; an empty set if no policies are found - * @throws SmartIdClientException if there is an error parsing the certificate policies - */ - public static Set getCertificatePolicy(X509Certificate certificate) { - Set result = new HashSet<>(); - byte[] extensionValue = certificate.getExtensionValue(CERTIFICATE_POLICY_OID); - if (extensionValue == null) { - return result; - } - try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { - ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); - try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { - CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); - for (PolicyInformation pi : policies.getPolicyInformation()) { - result.add(pi.getPolicyIdentifier().getId()); - } - } - } catch (IOException ex) { - throw new SmartIdClientException("Unable to parse certificate policies", ex); - } - return result; - } - - /** - * Checks if the certificate has KeyUsage extension with Non-Repudiation bit set - *

- * This method can be used to check if a certificate is valid for signing in case the certificate profile - * requires that Non-Repudiation bit must be set in KeyUsage extension. - * - * @param certificate the X.509 certificate to check - * @return true if the certificate does not have KeyUsage extension or does not have Non-Repudiation bit set; false otherwise - */ - public static boolean hasNonRepudiationKeyUsage(X509Certificate certificate) { - boolean[] keyUsage = certificate.getKeyUsage(); - return keyUsage != null && keyUsage.length > 1 && keyUsage[KEY_USAGE_NON_REPUDIATION_INDEX]; - } - - private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { - try { - return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); - } catch (IOException | ClassCastException e) { - logger.info("Could not extract date-of-birth from certificate attribute. It seems the attribute does not exist in certificate."); - } catch (ParseException e) { - logger.warn("Date of birth field existed in certificate but failed to parse the value"); - } - return Optional.empty(); - } - - private static Date getDateOfBirthFromAttributeInternal(X509Certificate x509Certificate) throws IOException, ParseException { - byte[] extensionValue = x509Certificate.getExtensionValue(Extension.subjectDirectoryAttributes.getId()); - - if (extensionValue == null) { - logger.debug("subjectDirectoryAttributes field (that carries date-of-birth value) not found from certificate"); - return null; - } - - DEROctetString derOctetString = toDEROctetString(extensionValue); - DLSequence sequence = toDLSequence(derOctetString.getOctets()); - Enumeration objects = ((DLSequence) sequence.getObjectAt(0)).getObjects(); - - while (objects.hasMoreElements()) { - Object param = objects.nextElement(); - - if (param instanceof ASN1ObjectIdentifier id) { - if (id.equals(BCStyle.DATE_OF_BIRTH) && objects.hasMoreElements()) { - Object nextElement = objects.nextElement(); - - DLSet x = ((DLSet) nextElement); - ASN1Encodable objectAt2 = x.getObjectAt(0); - - ASN1GeneralizedTime time = (ASN1GeneralizedTime) objectAt2; - return time.getDate(); - } - } - } - return null; - } - - private static DEROctetString toDEROctetString(byte[] data) throws IOException { - return (DEROctetString) toDerObject(data); - } - - private static DLSequence toDLSequence(byte[] data) throws IOException { - return (DLSequence) toDerObject(data); - } - - private static ASN1Primitive toDerObject(byte[] data) throws IOException { - ByteArrayInputStream inStream = new ByteArrayInputStream(data); - ASN1InputStream asnInputStream = new ASN1InputStream(inStream); - - return asnInputStream.readObject(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.DLSet; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for extracting attributes from X.509 certificates. + */ +public final class CertificateAttributeUtil { + + private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); + + private static final String CERTIFICATE_POLICY_OID = "2.5.29.32"; + private static final int KEY_USAGE_NON_REPUDIATION_INDEX = 1; + + private CertificateAttributeUtil() { + } + + /** + * Get Date-of-birth (DoB) from a specific certificate header (if present). + *

+ * NB! This attribute may be present on some newer certificates (since ~ May 2021) but not all. + * + * @param x509Certificate Certificate to read the date-of-birth attribute from + * @return Person date of birth or null if this attribute is not set. + * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. + */ + public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { + Optional dateOfBirth = getDateOfBirthCertificateAttribute(x509Certificate); + + return dateOfBirth.map(date -> date.toInstant().atZone(ZoneOffset.UTC).toLocalDate()).orElse(null); + } + + /** + * Get value of attribute in X.500 principal. + * + * @param distinguishedName X.500 distinguished name using the format defined in RFC 2253. + * @param oid Object Identifier (OID) of the attribute to extract + * @return Attribute value + */ + public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { + var x500name = new X500Name(distinguishedName); + RDN[] rdns = x500name.getRDNs(oid); + if (rdns.length == 0) { + return Optional.empty(); + } + return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); + } + + /** + * Extracts certificate policy OID from the given X.509 certificate. + * + * @param certificate the X.509 certificate from which to extract the policy OIDs + * @return a set of certificate policy OIDs as strings; an empty set if no policies are found + * @throws SmartIdClientException if there is an error parsing the certificate policies + */ + public static Set getCertificatePolicy(X509Certificate certificate) { + Set result = new HashSet<>(); + byte[] extensionValue = certificate.getExtensionValue(CERTIFICATE_POLICY_OID); + if (extensionValue == null) { + return result; + } + try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { + ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); + try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { + CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); + for (PolicyInformation pi : policies.getPolicyInformation()) { + result.add(pi.getPolicyIdentifier().getId()); + } + } + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse certificate policies", ex); + } + return result; + } + + /** + * Checks if the certificate has KeyUsage extension with Non-Repudiation bit set + *

+ * This method can be used to check if a certificate is valid for signing in case the certificate profile + * requires that Non-Repudiation bit must be set in KeyUsage extension. + * + * @param certificate the X.509 certificate to check + * @return true if the certificate does not have KeyUsage extension or does not have Non-Repudiation bit set; false otherwise + */ + public static boolean hasNonRepudiationKeyUsage(X509Certificate certificate) { + boolean[] keyUsage = certificate.getKeyUsage(); + return keyUsage != null && keyUsage.length > 1 && keyUsage[KEY_USAGE_NON_REPUDIATION_INDEX]; + } + + private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { + try { + return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); + } catch (IOException | ClassCastException e) { + logger.info("Could not extract date-of-birth from certificate attribute. It seems the attribute does not exist in certificate."); + } catch (ParseException e) { + logger.warn("Date of birth field existed in certificate but failed to parse the value"); + } + return Optional.empty(); + } + + private static Date getDateOfBirthFromAttributeInternal(X509Certificate x509Certificate) throws IOException, ParseException { + byte[] extensionValue = x509Certificate.getExtensionValue(Extension.subjectDirectoryAttributes.getId()); + + if (extensionValue == null) { + logger.debug("subjectDirectoryAttributes field (that carries date-of-birth value) not found from certificate"); + return null; + } + + DEROctetString derOctetString = toDEROctetString(extensionValue); + DLSequence sequence = toDLSequence(derOctetString.getOctets()); + Enumeration objects = ((DLSequence) sequence.getObjectAt(0)).getObjects(); + + while (objects.hasMoreElements()) { + Object param = objects.nextElement(); + + if (param instanceof ASN1ObjectIdentifier id) { + if (id.equals(BCStyle.DATE_OF_BIRTH) && objects.hasMoreElements()) { + Object nextElement = objects.nextElement(); + + DLSet x = ((DLSet) nextElement); + ASN1Encodable objectAt2 = x.getObjectAt(0); + + ASN1GeneralizedTime time = (ASN1GeneralizedTime) objectAt2; + return time.getDate(); + } + } + } + return null; + } + + private static DEROctetString toDEROctetString(byte[] data) throws IOException { + return (DEROctetString) toDerObject(data); + } + + private static DLSequence toDLSequence(byte[] data) throws IOException { + return (DLSequence) toDerObject(data); + } + + private static ASN1Primitive toDerObject(byte[] data) throws IOException { + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + ASN1InputStream asnInputStream = new ASN1InputStream(inStream); + + return asnInputStream.readObject(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/InteractionUtil.java b/src/main/java/ee/sk/smartid/util/InteractionUtil.java index 2ba91cbd..3d780327 100644 --- a/src/main/java/ee/sk/smartid/util/InteractionUtil.java +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -1,88 +1,88 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; -import java.util.Objects; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.Interaction; - -/** - * Utility for interactions related actions - */ -public class InteractionUtil { - - private static final ObjectMapper mapper = new ObjectMapper(); - - private InteractionUtil() { - } - - /** - * Encodes list of interactions to Base64-encoded string - * - * @param interactions list of interactions - * @return base64 encoded string - * @throws SmartIdClientException if unable to encode interactions - */ - public static String encodeToBase64(List interactions) { - try { - String json = mapper.writeValueAsString(interactions); - return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException ex) { - throw new SmartIdClientException("Unable to encode interactions to Base64", ex); - } - } - - /** - * Calculates SHA-256 digest of the interactions and encodes it to Base64 - * - * @param interactions interactions string - * @return base64 encoded SHA-256 digest - */ - public static String calculateDigest(String interactions){ - byte[] digest = DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - return Base64.getEncoder().encodeToString(digest); - } - - /** - * Checks if the list of interactions is empty or contains only null values - * - * @param interactions list of interactions - * @return true if the list is empty or contains only null values, false otherwise - */ - public static boolean isEmpty(List interactions) { - return interactions == null || interactions.stream().filter(Objects::nonNull).toList().isEmpty(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Utility for interactions related actions + */ +public class InteractionUtil { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private InteractionUtil() { + } + + /** + * Encodes list of interactions to Base64-encoded string + * + * @param interactions list of interactions + * @return base64 encoded string + * @throws SmartIdClientException if unable to encode interactions + */ + public static String encodeToBase64(List interactions) { + try { + String json = mapper.writeValueAsString(interactions); + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException ex) { + throw new SmartIdClientException("Unable to encode interactions to Base64", ex); + } + } + + /** + * Calculates SHA-256 digest of the interactions and encodes it to Base64 + * + * @param interactions interactions string + * @return base64 encoded SHA-256 digest + */ + public static String calculateDigest(String interactions){ + byte[] digest = DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getEncoder().encodeToString(digest); + } + + /** + * Checks if the list of interactions is empty or contains only null values + * + * @param interactions list of interactions + * @return true if the list is empty or contains only null values, false otherwise + */ + public static boolean isEmpty(List interactions) { + return interactions == null || interactions.stream().filter(Objects::nonNull).toList().isEmpty(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index 64ac36e7..eb59d3f0 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -1,142 +1,142 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.format.ResolverStyle; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Utility class for handling national identity numbers (personal codes). - */ -public class NationalIdentityNumberUtil { - - private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); - - private static final DateTimeFormatter DATE_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuuMMdd") - .withResolverStyle(ResolverStyle.STRICT); - - /** - * Detect date-of-birth from a Baltic national identification number if possible or return null. - *

- * This method always returns the value for all Estonian and Lithuanian national identification numbers. - *

- * It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 - * (starting with "32") do not carry date-of-birth. - *

- * For non-Baltic countries (countries other than Estonia, Latvia or Lithuania) it always returns null - * (even if it would be possible to deduce date of birth from national identity number). - *

- * Newer (but not all) Smart-ID certificates have date-of-birth on a separate attribute. - * It is recommended to use that value if present. - * - * @param authenticationIdentity Authentication identity - * @return DateOfBirth or null if it cannot be detected from personal code - * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) - */ - public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { - String identityNumber = authenticationIdentity.getIdentityNumber(); - - return switch (authenticationIdentity.getCountry().toUpperCase()) { - case "EE", "LT" -> parseEeLtDateOfBirth(identityNumber); - case "LV" -> parseLvDateOfBirth(identityNumber); - default -> null; - }; - } - - /** - * Parses date of birth from Estonian or Lithuanian national identity number. - * - * @param eeOrLtNationalIdentityNumber Estonian or Lithuanian national identity number - * @return Date of birth - * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed - */ - public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { - String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); - - birthDate = switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { - case "1", "2" -> "18" + birthDate; - case "3", "4" -> "19" + birthDate; - case "5", "6" -> "20" + birthDate; - default -> throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); - }; - - try { - return LocalDate.parse(birthDate, DATE_FORMATTER_YYYY_MM_DD); - } catch (DateTimeParseException e) { - throw new UnprocessableSmartIdResponseException("Could not parse birthdate from nationalIdentityNumber=" + eeOrLtNationalIdentityNumber, e); - } - } - - /** - * Parses date of birth from Latvian national identity number if possible. - *

- * Latvian personal codes issued after July 1st 2017 (starting with "32") do not carry date-of-birth and null is returned. - * - * @param lvNationalIdentityNumber Latvian national identity number - * @return Date of birth or null if the personal code does not carry birthdate info - * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed - */ - public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { - String birthDay = lvNationalIdentityNumber.substring(0, 2); - if (isNonParsableLVPersonCodePrefix(birthDay)) { - logger.debug("Person has newer type of Latvian ID-code that does not carry birthdate info"); - return null; - } - - String birthMonth = lvNationalIdentityNumber.substring(2, 4); - String birthYearTwoDigit = lvNationalIdentityNumber.substring(4, 6); - String century = lvNationalIdentityNumber.substring(7, 8); - String birthDateYyyyMmDd = switch (century) { - case "0" -> "18" + (birthYearTwoDigit + birthMonth + birthDay); - case "1" -> "19" + (birthYearTwoDigit + birthMonth + birthDay); - case "2" -> "20" + (birthYearTwoDigit + birthMonth + birthDay); - default -> throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); - }; - - try { - return LocalDate.parse(birthDateYyyyMmDd, DATE_FORMATTER_YYYY_MM_DD); - } catch (DateTimeParseException e) { - throw new UnprocessableSmartIdResponseException("Unable get birthdate from Latvian personal code " + lvNationalIdentityNumber, e); - } - } - - private static boolean isNonParsableLVPersonCodePrefix(String prefix) { - Pattern pattern = Pattern.compile("3[2-9]"); - Matcher matcher = pattern.matcher(prefix); - return matcher.matches(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Utility class for handling national identity numbers (personal codes). + */ +public class NationalIdentityNumberUtil { + + private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); + + private static final DateTimeFormatter DATE_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuuMMdd") + .withResolverStyle(ResolverStyle.STRICT); + + /** + * Detect date-of-birth from a Baltic national identification number if possible or return null. + *

+ * This method always returns the value for all Estonian and Lithuanian national identification numbers. + *

+ * It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 + * (starting with "32") do not carry date-of-birth. + *

+ * For non-Baltic countries (countries other than Estonia, Latvia or Lithuania) it always returns null + * (even if it would be possible to deduce date of birth from national identity number). + *

+ * Newer (but not all) Smart-ID certificates have date-of-birth on a separate attribute. + * It is recommended to use that value if present. + * + * @param authenticationIdentity Authentication identity + * @return DateOfBirth or null if it cannot be detected from personal code + * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) + */ + public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { + String identityNumber = authenticationIdentity.getIdentityNumber(); + + return switch (authenticationIdentity.getCountry().toUpperCase()) { + case "EE", "LT" -> parseEeLtDateOfBirth(identityNumber); + case "LV" -> parseLvDateOfBirth(identityNumber); + default -> null; + }; + } + + /** + * Parses date of birth from Estonian or Lithuanian national identity number. + * + * @param eeOrLtNationalIdentityNumber Estonian or Lithuanian national identity number + * @return Date of birth + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ + public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { + String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); + + birthDate = switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { + case "1", "2" -> "18" + birthDate; + case "3", "4" -> "19" + birthDate; + case "5", "6" -> "20" + birthDate; + default -> throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); + }; + + try { + return LocalDate.parse(birthDate, DATE_FORMATTER_YYYY_MM_DD); + } catch (DateTimeParseException e) { + throw new UnprocessableSmartIdResponseException("Could not parse birthdate from nationalIdentityNumber=" + eeOrLtNationalIdentityNumber, e); + } + } + + /** + * Parses date of birth from Latvian national identity number if possible. + *

+ * Latvian personal codes issued after July 1st 2017 (starting with "32") do not carry date-of-birth and null is returned. + * + * @param lvNationalIdentityNumber Latvian national identity number + * @return Date of birth or null if the personal code does not carry birthdate info + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ + public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { + String birthDay = lvNationalIdentityNumber.substring(0, 2); + if (isNonParsableLVPersonCodePrefix(birthDay)) { + logger.debug("Person has newer type of Latvian ID-code that does not carry birthdate info"); + return null; + } + + String birthMonth = lvNationalIdentityNumber.substring(2, 4); + String birthYearTwoDigit = lvNationalIdentityNumber.substring(4, 6); + String century = lvNationalIdentityNumber.substring(7, 8); + String birthDateYyyyMmDd = switch (century) { + case "0" -> "18" + (birthYearTwoDigit + birthMonth + birthDay); + case "1" -> "19" + (birthYearTwoDigit + birthMonth + birthDay); + case "2" -> "20" + (birthYearTwoDigit + birthMonth + birthDay); + default -> throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); + }; + + try { + return LocalDate.parse(birthDateYyyyMmDd, DATE_FORMATTER_YYYY_MM_DD); + } catch (DateTimeParseException e) { + throw new UnprocessableSmartIdResponseException("Unable get birthdate from Latvian personal code " + lvNationalIdentityNumber, e); + } + } + + private static boolean isNonParsableLVPersonCodePrefix(String prefix) { + Pattern pattern = Pattern.compile("3[2-9]"); + Matcher matcher = pattern.matcher(prefix); + return matcher.matches(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/SetUtil.java b/src/main/java/ee/sk/smartid/util/SetUtil.java index 9ea2a928..fe763903 100644 --- a/src/main/java/ee/sk/smartid/util/SetUtil.java +++ b/src/main/java/ee/sk/smartid/util/SetUtil.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Utility class for Set operations. - */ -public final class SetUtil { - - private SetUtil() { - } - - /** - * Converts an array to a Set, filtering out null or empty values. - * - * @param array array to be converted - * @return a set of non-null, non-empty trimmed strings - */ - public static Set toSet(String[] array) { - return Arrays.stream(array) - .filter(Objects::nonNull) - .map(String::trim) - .filter(StringUtil::isNotEmpty) - .collect(Collectors.toSet()); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for Set operations. + */ +public final class SetUtil { + + private SetUtil() { + } + + /** + * Converts an array to a Set, filtering out null or empty values. + * + * @param array array to be converted + * @return a set of non-null, non-empty trimmed strings + */ + public static Set toSet(String[] array) { + return Arrays.stream(array) + .filter(Objects::nonNull) + .map(String::trim) + .filter(StringUtil::isNotEmpty) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index 6274c4e8..0b0fb943 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -1,66 +1,66 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Utility class to handle string operations - */ -public final class StringUtil { - - private StringUtil() { - } - - /** - * Checks that given CharSequence is not null and not empty - * - * @param cs the CharSequence to check - * @return true if the CharSequence is not null and not empty, false otherwise - */ - public static boolean isNotEmpty(final CharSequence cs) { - return cs != null && !cs.isEmpty(); - } - - /** - * Checks that given CharSequence is null or empty - * - * @param cs the CharSequence to check - * @return true if the CharSequence is null or empty, false otherwise - */ - public static boolean isEmpty(final CharSequence cs) { - return cs == null || cs.isEmpty(); - } - - /** - * Checks that given string is not null and not empty - * - * @param input the value to check - * @return String if the input is not null and not empty, empty string otherwise - */ - public static String orEmpty(String input) { - return input == null ? "" : input; - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Utility class to handle string operations + */ +public final class StringUtil { + + private StringUtil() { + } + + /** + * Checks that given CharSequence is not null and not empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is not null and not empty, false otherwise + */ + public static boolean isNotEmpty(final CharSequence cs) { + return cs != null && !cs.isEmpty(); + } + + /** + * Checks that given CharSequence is null or empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is null or empty, false otherwise + */ + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.isEmpty(); + } + + /** + * Checks that given string is not null and not empty + * + * @param input the value to check + * @return String if the input is not null and not empty, empty string otherwise + */ + public static String orEmpty(String input) { + return input == null ? "" : input; + } +} diff --git a/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt b/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt index aba475f6..62466c24 100644 --- a/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt +++ b/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIG4jCCBcqgAwIBAgIQO4A6a2nBKoxXxVAFMRvE2jANBgkqhkiG9w0BAQwFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MjEwOVoYDzIwMzAxMjE3 -MjM1OTU5WjBgMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFDASBgNVBAMMC0VJ -RC1TSyAyMDE2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7XWFN0j -1CFoGIuVe9xRezEnA0Tk3vmvIpvURX+y7Z5DJsfub2mtpSLtbhXjAeynq9QV78zj -gQ73pNVGh+GQ6oPG7HF8KIlZuIYsf1+gBxPxNiLa0+sCWxa6p4HQbgdgYRVGod4I -Qbib9KbOki3wjCG5WiWh1SP9qcuTZVY+9zawkSMf65Px/Y4ChjtNFtY66MEvsPCh -lHHfsBNiUbtZ68jJNYCECjtkm0vxz2iiSXB2WRIv3/hTrRgMJ2CNMyFjRQoGQlpH -010+fcisObKeyPwA8kI22Oto9MzLw7KsY524OD3B1L5MExYxHD916XIEHT/9gBP2 -Zn8qZu/BllKdSIapOIJW9ZEw+3w5UOU6LT3tTSbAzeQAnD3eCABPifYwHYC0lmKs -PpQJqtx0Q3Jbm3BGReYiZ9KuK36nF/G78YjhM+yioERr2B/cKf31j0W/GuGvyHak -bokwy7nsbL30sTuRLR70Oqi5UBMy4e8J2CduR3R3NJw5UqpScJIchngsLAx+WsyC -0w38AmMewMBcnlp/QbakKo52HrsYRR1m+NhCVDBy45Lzl8I0/OGd9Ikdg1h7T7SI -guZVpyzys8E0yfrcS5YMEd9hMqVPr7rszXCzbxyw0tVIk8QLMw/lI+XE1Oi7Skgz -A2i5Vpa6i2K0ard6GPHzRqGPTkjc5Z4DzZMCAwEAAaOCAn8wggJ7MB8GA1UdIwQY -MBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBScCagHhww9rC6H/KCu -0vtlSYgo+zAOBgNVHQ8BAf8EBAMCAQYwgcQGA1UdIASBvDCBuTA8BgcEAIvsQAEC -MDEwLwYIKwYBBQUHAgEWI2h0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0v -Q1BTMDwGBwQAi+xAAQAwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUv -cmVwb3NpdG9vcml1bS9DUFMwOwYGBACPegECMDEwLwYIKwYBBQUHAgEWI2h0dHBz -Oi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMBIGA1UdEwEB/wQIMAYBAf8C -AQAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB8Bggr -BgEFBQcBAQRwMG4wIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3NwLnNrLmVlL0NBMEoG -CCsGAQUFBzAChj5odHRwOi8vd3d3LnNrLmVlL2NlcnRzL0VFX0NlcnRpZmljYXRp -b25fQ2VudHJlX1Jvb3RfQ0EuZGVyLmNydDBBBgNVHR4EOjA4oTYwBIICIiIwCocI -AAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJQYI -KwYBBQUHAQMEGTAXMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwPQYDVR0fBDYwNDAy -oDCgLoYsaHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5j -cmwwDQYJKoZIhvcNAQEMBQADggEBAKSIoud5DSfhDU6yp+VrXYL40wi5zFTf19ha -/kO/zzLxZ1hf45VJmSyukMWaWXEqhaLWBZuw5kP78mQ0HyaRUennN0hom/pEiBz6 -cuz9oc+xlmPAZM25ZoaLqa4upP2/+NCWoRTzYkIdc9MEECs5RMBUmyT1G4s8J6n8 -L2M2yYadBMvPGJS3yXxYdc/b3a2foiw3kKa/q1tXAHXZCsuxFVYxXdZt3AwInYHe -mCVKjZg8BaRpvIEXd3AgJwt+9bpV/x0/MouRPNRv0jjWIx1sAlL94hO74WZDMFbZ -VaV6gpG77X2P3dPHKFIRWzjtSQJX4C5n1uvQBxO4ABoMswq0lq0= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG4jCCBcqgAwIBAgIQO4A6a2nBKoxXxVAFMRvE2jANBgkqhkiG9w0BAQwFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MjEwOVoYDzIwMzAxMjE3 +MjM1OTU5WjBgMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFDASBgNVBAMMC0VJ +RC1TSyAyMDE2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7XWFN0j +1CFoGIuVe9xRezEnA0Tk3vmvIpvURX+y7Z5DJsfub2mtpSLtbhXjAeynq9QV78zj +gQ73pNVGh+GQ6oPG7HF8KIlZuIYsf1+gBxPxNiLa0+sCWxa6p4HQbgdgYRVGod4I +Qbib9KbOki3wjCG5WiWh1SP9qcuTZVY+9zawkSMf65Px/Y4ChjtNFtY66MEvsPCh +lHHfsBNiUbtZ68jJNYCECjtkm0vxz2iiSXB2WRIv3/hTrRgMJ2CNMyFjRQoGQlpH +010+fcisObKeyPwA8kI22Oto9MzLw7KsY524OD3B1L5MExYxHD916XIEHT/9gBP2 +Zn8qZu/BllKdSIapOIJW9ZEw+3w5UOU6LT3tTSbAzeQAnD3eCABPifYwHYC0lmKs +PpQJqtx0Q3Jbm3BGReYiZ9KuK36nF/G78YjhM+yioERr2B/cKf31j0W/GuGvyHak +bokwy7nsbL30sTuRLR70Oqi5UBMy4e8J2CduR3R3NJw5UqpScJIchngsLAx+WsyC +0w38AmMewMBcnlp/QbakKo52HrsYRR1m+NhCVDBy45Lzl8I0/OGd9Ikdg1h7T7SI +guZVpyzys8E0yfrcS5YMEd9hMqVPr7rszXCzbxyw0tVIk8QLMw/lI+XE1Oi7Skgz +A2i5Vpa6i2K0ard6GPHzRqGPTkjc5Z4DzZMCAwEAAaOCAn8wggJ7MB8GA1UdIwQY +MBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBScCagHhww9rC6H/KCu +0vtlSYgo+zAOBgNVHQ8BAf8EBAMCAQYwgcQGA1UdIASBvDCBuTA8BgcEAIvsQAEC +MDEwLwYIKwYBBQUHAgEWI2h0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0v +Q1BTMDwGBwQAi+xAAQAwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUv +cmVwb3NpdG9vcml1bS9DUFMwOwYGBACPegECMDEwLwYIKwYBBQUHAgEWI2h0dHBz +Oi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMBIGA1UdEwEB/wQIMAYBAf8C +AQAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB8Bggr +BgEFBQcBAQRwMG4wIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3NwLnNrLmVlL0NBMEoG +CCsGAQUFBzAChj5odHRwOi8vd3d3LnNrLmVlL2NlcnRzL0VFX0NlcnRpZmljYXRp +b25fQ2VudHJlX1Jvb3RfQ0EuZGVyLmNydDBBBgNVHR4EOjA4oTYwBIICIiIwCocI +AAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJQYI +KwYBBQUHAQMEGTAXMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwPQYDVR0fBDYwNDAy +oDCgLoYsaHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5j +cmwwDQYJKoZIhvcNAQEMBQADggEBAKSIoud5DSfhDU6yp+VrXYL40wi5zFTf19ha +/kO/zzLxZ1hf45VJmSyukMWaWXEqhaLWBZuw5kP78mQ0HyaRUennN0hom/pEiBz6 +cuz9oc+xlmPAZM25ZoaLqa4upP2/+NCWoRTzYkIdc9MEECs5RMBUmyT1G4s8J6n8 +L2M2yYadBMvPGJS3yXxYdc/b3a2foiw3kKa/q1tXAHXZCsuxFVYxXdZt3AwInYHe +mCVKjZg8BaRpvIEXd3AgJwt+9bpV/x0/MouRPNRv0jjWIx1sAlL94hO74WZDMFbZ +VaV6gpG77X2P3dPHKFIRWzjtSQJX4C5n1uvQBxO4ABoMswq0lq0= +-----END CERTIFICATE----- diff --git a/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt b/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt index b300155e..e9cb6d63 100644 --- a/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt +++ b/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt @@ -1,37 +1,37 @@ ------BEGIN CERTIFICATE----- -MIIGYjCCBUqgAwIBAgIQV6nz7KIvDihXxU71YTbgWjANBgkqhkiG9w0BAQwFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MTYzN1oYDzIwMzAxMjE3 -MjM1OTU5WjBfMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzARBgNVBAMMCk5R -LVNLIDIwMTYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdkRRNDxfO -6oKU9GDrGLNQc41PA+pqDKCEcDhSw1bnkC/nDumg4PawQk8xklyDHr2ShrsFrTo5 -wps5UcgxxTMqb98bmMxQYghqxu5NqqpaZopbbSj+qDYUzrZkXIlVe+HFpUt5ce9W -NpEmeenVAlt4ZaN1/srDfv3NSMmcF2r9XiUIIhDavxQ+QgPy3CrgT0Ja3yw/PLpF -/ajCNQWaGWJHYkgNVzrnrKhKYDhgorc3lSqGfTfhW2Xf5klvBZokPfbhD26csnPe -JjQQQJ2Loot3Z9/QPzfY/Qnqp5hjkvfqjKksX2wAt/UB+Hk4sRG+6Nqa3b+gxqMc -ih1eI/I93Ii6OC7LijhN2k0R9L5+ArgQXhlAQYZGeCAC/unHmpCkiUQrEJq27kst -mzoENnwQnF3mhq81KQGZul/Guw1fsQOolALESEWG6dTP1szaLeba4LYN707b9puR -OVXk1WLoau131KZnIdc/+Ktu2ni4SVL3+qKbJ7+oqIfiFAqlSuCPTKssdFC49m7V -G4bXnrYeA5svUQjCvpANmzXqRs6DmdctKPuXUj+W/gnQNoLOvIEkK30TD/RKd4eh -uzzYj9qirhqBDFg+Ipqh9OByK7aY6f9KZ6qKmKttcPb4R7arBtuQoBlqadcXoGig -o/kr/iXVRabWfGVM73iQo36RZrklrSu5awIDAQABo4ICADCCAfwwHwYDVR0jBBgw -FoAUEvJaPupWHL/NBqzx8SXJqUvUFJkwHQYDVR0OBBYEFHq3hV+h88xBt67p6gZR -CuD5AsisMA4GA1UdDwEB/wQEAwIBBjBGBgNVHSAEPzA9MDsGBgQAj3oBATAxMC8G -CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS -BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD -AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v -b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0 -cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e -BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs -SQEBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9y -eS9jcmxzL2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQCu4HLsEBBpKmXw -agXpFkmEGqTOC/eYWrtwVZNnz/MB+z8c6TyxwW2cDmNwMwMfojXT447rQ/xlai/5 -gjGkRwRE8P5W90h/JkO3rUWG4asrvPAwmkIiUHsHIHDVHCsSmhLNEgPdM4zP88/L -EmV89ZIvUWGjzZYcgBljYeAlK4dCy4/7U14JW9FCwvFjFOyfDcpoYwxbV7Jkbhsw -9J8uzzxjspGCvoq5izeTGuRV+WtV+yy6W/UOnpmYOJ6jxzUoYq6fnQGU+J9CLVxj -jE8Jj25fuWSU3BvPs8cms7RzvvuZvPgQWm8IZt6L5P6EHOzeER3m3nftERhG2OXE -+MHJ+onc ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGYjCCBUqgAwIBAgIQV6nz7KIvDihXxU71YTbgWjANBgkqhkiG9w0BAQwFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MTYzN1oYDzIwMzAxMjE3 +MjM1OTU5WjBfMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzARBgNVBAMMCk5R +LVNLIDIwMTYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdkRRNDxfO +6oKU9GDrGLNQc41PA+pqDKCEcDhSw1bnkC/nDumg4PawQk8xklyDHr2ShrsFrTo5 +wps5UcgxxTMqb98bmMxQYghqxu5NqqpaZopbbSj+qDYUzrZkXIlVe+HFpUt5ce9W +NpEmeenVAlt4ZaN1/srDfv3NSMmcF2r9XiUIIhDavxQ+QgPy3CrgT0Ja3yw/PLpF +/ajCNQWaGWJHYkgNVzrnrKhKYDhgorc3lSqGfTfhW2Xf5klvBZokPfbhD26csnPe +JjQQQJ2Loot3Z9/QPzfY/Qnqp5hjkvfqjKksX2wAt/UB+Hk4sRG+6Nqa3b+gxqMc +ih1eI/I93Ii6OC7LijhN2k0R9L5+ArgQXhlAQYZGeCAC/unHmpCkiUQrEJq27kst +mzoENnwQnF3mhq81KQGZul/Guw1fsQOolALESEWG6dTP1szaLeba4LYN707b9puR +OVXk1WLoau131KZnIdc/+Ktu2ni4SVL3+qKbJ7+oqIfiFAqlSuCPTKssdFC49m7V +G4bXnrYeA5svUQjCvpANmzXqRs6DmdctKPuXUj+W/gnQNoLOvIEkK30TD/RKd4eh +uzzYj9qirhqBDFg+Ipqh9OByK7aY6f9KZ6qKmKttcPb4R7arBtuQoBlqadcXoGig +o/kr/iXVRabWfGVM73iQo36RZrklrSu5awIDAQABo4ICADCCAfwwHwYDVR0jBBgw +FoAUEvJaPupWHL/NBqzx8SXJqUvUFJkwHQYDVR0OBBYEFHq3hV+h88xBt67p6gZR +CuD5AsisMA4GA1UdDwEB/wQEAwIBBjBGBgNVHSAEPzA9MDsGBgQAj3oBATAxMC8G +CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS +BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD +AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v +b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0 +cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e +BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs +SQEBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9y +eS9jcmxzL2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQCu4HLsEBBpKmXw +agXpFkmEGqTOC/eYWrtwVZNnz/MB+z8c6TyxwW2cDmNwMwMfojXT447rQ/xlai/5 +gjGkRwRE8P5W90h/JkO3rUWG4asrvPAwmkIiUHsHIHDVHCsSmhLNEgPdM4zP88/L +EmV89ZIvUWGjzZYcgBljYeAlK4dCy4/7U14JW9FCwvFjFOyfDcpoYwxbV7Jkbhsw +9J8uzzxjspGCvoq5izeTGuRV+WtV+yy6W/UOnpmYOJ6jxzUoYq6fnQGU+J9CLVxj +jE8Jj25fuWSU3BvPs8cms7RzvvuZvPgQWm8IZt6L5P6EHOzeER3m3nftERhG2OXE ++MHJ+onc +-----END CERTIFICATE----- diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java index 2da25525..001a92ff 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java @@ -1,54 +1,54 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -import org.junit.jupiter.api.Test; - -class AuthenticationIdentityMapperTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - - @Test - void from() { - X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); - AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); - - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("40504040001", authenticationIdentity.getIdentityNumber()); - assertEquals("EE", authenticationIdentity.getCountry()); - - assertEquals(certificate, authenticationIdentity.getAuthCertificate()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class AuthenticationIdentityMapperTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + @Test + void from() { + X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); + AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); + + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("40504040001", authenticationIdentity.getIdentityNumber()); + assertEquals("EE", authenticationIdentity.getCountry()); + + assertEquals(certificate, authenticationIdentity.getAuthCertificate()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java index c18f84dd..6406af74 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; - -public class AuthenticationIdentityTest { - - @Test - public void getIdentityCode() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setIdentityNumber("identityNumber"); - - assertThat(authenticationIdentity.getIdentityCode(), is("identityNumber")); - } - - @Test - public void setIdentityCode() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setIdentityCode("identityCode"); - - assertThat(authenticationIdentity.getIdentityNumber(), is("identityCode")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +public class AuthenticationIdentityTest { + + @Test + public void getIdentityCode() { + AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); + authenticationIdentity.setIdentityNumber("identityNumber"); + + assertThat(authenticationIdentity.getIdentityCode(), is("identityNumber")); + } + + @Test + public void setIdentityCode() { + AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); + authenticationIdentity.setIdentityCode("identityCode"); + + assertThat(authenticationIdentity.getIdentityNumber(), is("identityCode")); + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index a671136a..dfcd6626 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -1,837 +1,837 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; - -class AuthenticationResponseMapperImplTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - - private AuthenticationResponseMapper authenticationResponseMapper; - - @BeforeEach - void setUp() { - authenticationResponseMapper = new AuthenticationResponseMapperImpl(); - } - - @Test - void from() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - } - - @ParameterizedTest - @EnumSource(FlowType.class) - void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - sessionSignature.setFlowType(flowType.getDescription()); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); - sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); - sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - assertEquals(hashAlgorithm, authenticationResponse.getRsaSsaPssSignatureParameters().getDigestHashAlgorithm()); - assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getRsaSsaPssSignatureParameters().getSaltLength()); - } - - @Test - void from_sessionStatusNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); - assertEquals("Parameter 'sessionsStatus' is not provided", exception.getMessage()); - } - - @Nested - class ValidateResult { - - @Test - void from_sessionResultIsNotPresent_throwException() { - var sessionStatus = new SessionStatus(); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result' is empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_endResultIsNotPresent_throwException(String endResult) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result.endResult' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void from_endResultIsError_throwException(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @NullAndEmptySource - void from_documentNumberIsEmpty_throwException(String documentNumber) { - var sessionResult = toSessionResult(documentNumber); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result.documentNumber' is empty", exception.getMessage()); - } - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(signatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signatureProtocol' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) - void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(invalidSignatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signatureProtocol' has unsupported value", exception.getMessage()); - } - - @Nested - class ValidateSignature { - - @Test - void from_signatureIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureValueIsNotProvided_throwException(String signatureValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.value' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\|invalidSignatureValue|", "#1234567890"}) - void from_signatureValueDoesNotMatchThePattern_throwException(String signatureValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.value' does not have Base64-encoded value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_serverRandomIsNotProvided_throwException(String serverRandom) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom(serverRandom); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' is empty", exception.getMessage()); - } - - @Test - void from_serverRandomLengthIsLessThanAllowed_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(23)); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' value length is less than required", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\|YXRsZWFzdDI0Y2hhcmFjdGVycw|", "#YXRsZWFzdDI0Y2hhcmFjdGVycw"}) - void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRandom) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom(serverRandom); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_userChallengeIsEmpty_throwException(String userChallenge) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge(userChallenge); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.userChallenge' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\#dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsd", "dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdW="}) - void from_providedUserChallengeDoesNotMatchThePattern_throwException(String userChallenge) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge(userChallenge); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.userChallenge' value does not match required pattern", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_flowTypeNotProvided_throwException(String flowType) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType(flowType); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.flowType' is empty", exception.getMessage()); - } - - @Test - void from_flowTypeNotSupported_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.flowType' has unsupported value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithm); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithm' is empty", exception.getMessage()); - } - - @Test - void from_signatureAlgorithmIsNotSupported_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithm' has unsupported value", exception.getMessage()); - } - - @Nested - class ValidateSignatureAlgorithmParameters { - - @Test - void from_signatureAlgorithmParametersAreMissing_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(null); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "invalid"}) - void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(invalidHashAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_masGenAlgorithmIsMissing_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(algorithm); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", exception.getMessage()); - } - - @Test - void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("invalid"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_parametersInMaskGenAlgorithmAreMissing_throwException() { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(null); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_hashAlgorithmInMaskGenAlgorithmParametersIsEmpty_throwException(String hashAlgorithm) { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "asdhfasdf"}) - void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String hashAlgorithm) { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); - } - - @Test - void from_saltLengthIsMissing_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(null); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty", exception.getMessage()); - } - - @Test - void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(20); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_trailerFieldIsEmpty_throwException(String trailerField) { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField(trailerField); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty", exception.getMessage()); - } - - @Test - void from_trailerFieldValueIsInvalid_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField("invalid"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); - } - - private static SessionSignature toSessionSignature(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - return sessionSignature; - } - } - - private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature) { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - return sessionStatus; - } - - private static SessionMaskGenAlgorithmParameters toMaskGenAlgorithmParameters() { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - return maskGenAlgorithmParameters; - } - } - - @Nested - class ValidateCertificate { - - @Test - void from_sessionCertificateIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.value' is empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.certificateLevel' is empty", exception.getMessage()); - } - - @Test - void from_certificateIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); - } - - @Test - void from_certificateLevelIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "invalid"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.certificateLevel' has unsupported value", exception.getMessage()); - } - } - - @ParameterizedTest - @NullAndEmptySource - void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUsed) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed(interactionFlowUsed); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'interactionTypeUsed' is empty", exception.getMessage()); - } - - private static SessionResult toSessionResult(String documentNumber) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber(documentNumber); - return sessionResult; - } - - private static SessionSignature toSessionSignature(String signatureAlgorithm) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("U2VydmVyUmFuZG9tTW9yZVRoYW4yNENoYXJhY3RlcnM="); - sessionSignature.setUserChallenge("dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdWU"); - - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setFlowType("QR"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField("0xbc"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - return sessionSignature; - } - - private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(AUTH_CERT); - sessionCertificate.setCertificateLevel(QUALIFIED); - return sessionCertificate; - } - - private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - sessionStatus.setDeviceIpAddress("0.0.0.0"); - return sessionStatus; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class AuthenticationResponseMapperImplTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + private AuthenticationResponseMapper authenticationResponseMapper; + + @BeforeEach + void setUp() { + authenticationResponseMapper = new AuthenticationResponseMapperImpl(); + } + + @Test + void from() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @ParameterizedTest + @EnumSource(FlowType.class) + void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.setFlowType(flowType.getDescription()); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + assertEquals(hashAlgorithm, authenticationResponse.getRsaSsaPssSignatureParameters().getDigestHashAlgorithm()); + assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getRsaSsaPssSignatureParameters().getSaltLength()); + } + + @Test + void from_sessionStatusNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); + assertEquals("Parameter 'sessionsStatus' is not provided", exception.getMessage()); + } + + @Nested + class ValidateResult { + + @Test + void from_sessionResultIsNotPresent_throwException() { + var sessionStatus = new SessionStatus(); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result' is empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsNotPresent_throwException(String endResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result.endResult' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_endResultIsError_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @NullAndEmptySource + void from_documentNumberIsEmpty_throwException(String documentNumber) { + var sessionResult = toSessionResult(documentNumber); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result.documentNumber' is empty", exception.getMessage()); + } + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(signatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signatureProtocol' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) + void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(invalidSignatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signatureProtocol' has unsupported value", exception.getMessage()); + } + + @Nested + class ValidateSignature { + + @Test + void from_signatureIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureValueIsNotProvided_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.value' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|invalidSignatureValue|", "#1234567890"}) + void from_signatureValueDoesNotMatchThePattern_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.value' does not have Base64-encoded value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_serverRandomIsNotProvided_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' is empty", exception.getMessage()); + } + + @Test + void from_serverRandomLengthIsLessThanAllowed_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(23)); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' value length is less than required", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|YXRsZWFzdDI0Y2hhcmFjdGVycw|", "#YXRsZWFzdDI0Y2hhcmFjdGVycw"}) + void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_userChallengeIsEmpty_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.userChallenge' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\#dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsd", "dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdW="}) + void from_providedUserChallengeDoesNotMatchThePattern_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.userChallenge' value does not match required pattern", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_flowTypeNotProvided_throwException(String flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType(flowType); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.flowType' is empty", exception.getMessage()); + } + + @Test + void from_flowTypeNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.flowType' has unsupported value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithm); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' is empty", exception.getMessage()); + } + + @Test + void from_signatureAlgorithmIsNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' has unsupported value", exception.getMessage()); + } + + @Nested + class ValidateSignatureAlgorithmParameters { + + @Test + void from_signatureAlgorithmParametersAreMissing_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(null); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(invalidHashAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_masGenAlgorithmIsMissing_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(algorithm); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", exception.getMessage()); + } + + @Test + void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("invalid"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_parametersInMaskGenAlgorithmAreMissing_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(null); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmInMaskGenAlgorithmParametersIsEmpty_throwException(String hashAlgorithm) { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "asdhfasdf"}) + void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String hashAlgorithm) { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); + } + + @Test + void from_saltLengthIsMissing_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(null); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty", exception.getMessage()); + } + + @Test + void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(20); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_trailerFieldIsEmpty_throwException(String trailerField) { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField(trailerField); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty", exception.getMessage()); + } + + @Test + void from_trailerFieldValueIsInvalid_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("invalid"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); + } + + private static SessionSignature toSessionSignature(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + return sessionStatus; + } + + private static SessionMaskGenAlgorithmParameters toMaskGenAlgorithmParameters() { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + return maskGenAlgorithmParameters; + } + } + + @Nested + class ValidateCertificate { + + @Test + void from_sessionCertificateIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.value' is empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.certificateLevel' is empty", exception.getMessage()); + } + + @Test + void from_certificateIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); + } + + @Test + void from_certificateLevelIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.certificateLevel' has unsupported value", exception.getMessage()); + } + } + + @ParameterizedTest + @NullAndEmptySource + void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUsed) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed(interactionFlowUsed); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'interactionTypeUsed' is empty", exception.getMessage()); + } + + private static SessionResult toSessionResult(String documentNumber) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber(documentNumber); + return sessionResult; + } + + private static SessionSignature toSessionSignature(String signatureAlgorithm) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("U2VydmVyUmFuZG9tTW9yZVRoYW4yNENoYXJhY3RlcnM="); + sessionSignature.setUserChallenge("dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdWU"); + + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setFlowType("QR"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("0xbc"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + + private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(AUTH_CERT); + sessionCertificate.setCertificateLevel(QUALIFIED); + return sessionCertificate; + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + sessionStatus.setDeviceIpAddress("0.0.0.0"); + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java index dcc14ced..f6cf0af7 100644 --- a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -public class CapabilitiesArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")), - Arguments.of(new String[]{"capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), - Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"", "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{" ", "capability1"}, Set.of("capability1")) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class CapabilitiesArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")), + Arguments.of(new String[]{"capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), + Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{" ", "capability1"}, Set.of("capability1")) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java index 3634acfa..158fec61 100644 --- a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java @@ -1,290 +1,290 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateInfo; -import ee.sk.smartid.rest.dao.CertificateResponse; - -class CertificateByDocumentNumberRequestBuilderTest { - - private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - private static final String RP_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String RP_NAME = "DEMO"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void getCertificateByDocumentNumber_ok() { - CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); - - CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .getCertificateByDocumentNumber(); - - assertNotNull(result); - assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); - assertNotNull(result.certificate()); - - String subject = result.certificate().getSubjectX500Principal().getName(); - assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); - verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); - - CertificateByDocumentNumberRequest sentRequest = captor.getValue(); - assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); - assertEquals(RP_NAME, sentRequest.relyingPartyName()); - assertEquals("QUALIFIED", sentRequest.certificateLevel()); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelSetToNull_ok() { - CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); - - CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withCertificateLevel(null) - .getCertificateByDocumentNumber(); - - assertNotNull(result); - assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); - assertNotNull(result.certificate()); - - String subject = result.certificate().getSubjectX500Principal().getName(); - assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); - verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); - - CertificateByDocumentNumberRequest sentRequest = captor.getValue(); - assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); - assertEquals(RP_NAME, sentRequest.relyingPartyName()); - assertNull(sentRequest.certificateLevel()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_documentNumberMissing_throwException(String documentNumber) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withDocumentNumber(documentNumber); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_relyingPartyUUIDMissing_throwException(String uuid) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyName(RP_NAME) - .withRelyingPartyUUID(uuid); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_relyingPartyNameMissing_throwException(String relyingPartyName) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(relyingPartyName); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @Test - void getCertificateByDocumentNumber_responseIsNull_throwException() { - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response is not provided", ex.getMessage()); - } - - @Nested - class ValidateState { - - @Test - void getCertificateByDocumentNumber_responseStateMissing_throwException() { - var certificateResponse = new CertificateResponse(null, null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseStateValueIsInvalid_throwException() { - var certificateResponse = new CertificateResponse("invalid", null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseStateIsDocumentUnusable_throwException() { - var certificateResponse = new CertificateResponse(CertificateState.DOCUMENT_UNUSABLE.name(), null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); - } - } - - @Test - void getCertificateByDocumentNumber_certFieldMissing_throwException() { - var certificateResponse = new CertificateResponse(CertificateState.OK.name(), null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert' is missing", ex.getMessage()); - } - - @Nested - class ValidateCertificateLevel { - - @Test - void getCertificateByDocumentNumber_responseCertificateLevelMissing_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.certificateLevel' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseCertificateHasInvalidValue_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, "invalid"); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.certificateLevel' has unsupported value", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelLowerThanRequested_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate has lower level than requested", ex.getMessage()); - } - } - - @Test - void getCertificateByDocumentNumber_certValueMissing_throwException() { - CertificateResponse response = toCertificateResponse(null, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.value' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_certValueInvalidBase64_throwException() { - CertificateResponse certificateResponse = toCertificateResponse("NOT@BASE64!", CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.value' does not have Base64-encoded value", ex.getMessage()); - } - } - - private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() { - return new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME); - } - - private CertificateResponse toCertificateResponse(String certValue, String level) { - var certificate = new CertificateInfo(certValue, level); - return new CertificateResponse(CertificateState.OK.name(), certificate); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateInfo; +import ee.sk.smartid.rest.dao.CertificateResponse; + +class CertificateByDocumentNumberRequestBuilderTest { + + private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String RP_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RP_NAME = "DEMO"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void getCertificateByDocumentNumber_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertEquals("QUALIFIED", sentRequest.certificateLevel()); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelSetToNull_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(null) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertNull(sentRequest.certificateLevel()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_documentNumberMissing_throwException(String documentNumber) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withDocumentNumber(documentNumber); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_relyingPartyUUIDMissing_throwException(String uuid) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyName(RP_NAME) + .withRelyingPartyUUID(uuid); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_relyingPartyNameMissing_throwException(String relyingPartyName) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(relyingPartyName); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @Test + void getCertificateByDocumentNumber_responseIsNull_throwException() { + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response is not provided", ex.getMessage()); + } + + @Nested + class ValidateState { + + @Test + void getCertificateByDocumentNumber_responseStateMissing_throwException() { + var certificateResponse = new CertificateResponse(null, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseStateValueIsInvalid_throwException() { + var certificateResponse = new CertificateResponse("invalid", null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseStateIsDocumentUnusable_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.DOCUMENT_UNUSABLE.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); + } + } + + @Test + void getCertificateByDocumentNumber_certFieldMissing_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.OK.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert' is missing", ex.getMessage()); + } + + @Nested + class ValidateCertificateLevel { + + @Test + void getCertificateByDocumentNumber_responseCertificateLevelMissing_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseCertificateHasInvalidValue_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, "invalid"); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelLowerThanRequested_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate has lower level than requested", ex.getMessage()); + } + } + + @Test + void getCertificateByDocumentNumber_certValueMissing_throwException() { + CertificateResponse response = toCertificateResponse(null, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.value' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_certValueInvalidBase64_throwException() { + CertificateResponse certificateResponse = toCertificateResponse("NOT@BASE64!", CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.value' does not have Base64-encoded value", ex.getMessage()); + } + } + + private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() { + return new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME); + } + + private CertificateResponse toCertificateResponse(String certValue, String level) { + var certificate = new CertificateInfo(certValue, level); + return new CertificateResponse(CertificateState.OK.name(), certificate); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java index 9a1b1d21..b3de3d7b 100644 --- a/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java @@ -1,290 +1,290 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionStatus; - -public class CertificateChoiceResponseValidatorTest { - - private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - - CertificateChoiceResponseValidator certificateChoiceResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - } - - @Test - void validate() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void validate_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void validate_returnedCertificateHigherThanRequested_ok() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void validate_nqCertificate() { - var sessionStatus = toSessionStatus(NQ_SIGNING_CERTIFICATE, "ADVANCED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - - assertEquals("OK", response.getEndResult()); - assertEquals(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE), response.getCertificate()); - assertEquals(CertificateLevel.ADVANCED, response.getCertificateLevel()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_requestCertificateLevelNotProvided_throwException() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus, null)); - assertEquals("Parameter 'requestedCertificateLevel' is not provided", ex.getMessage()); - } - } - - @Nested - class ValidateEndResult { - - @Test - void validate_sessionResultIsNotProvided_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(new SessionStatus())); - assertEquals("Certificate choice session status field 'result' is missing", ex.getMessage()); - } - - @Test - void validate_sessionEndResultIsNotProvided_throwException() { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(new SessionResult()); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'result.endResult' is empty", ex.getMessage()); - } - - @Test - void validate_sessionDocumentNumberIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'result.documentNumber' is empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void validate_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - } - } - - @Nested - class ValidateCertificate { - - @Test - void validate_sessionCertificateIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.value' has empty value", ex.getMessage()); - } - - @Test - void validate_sessionCertificateLevelIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("INVALID"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.certificateLevel' has empty value", ex.getMessage()); - } - - @Test - void validate_sessionCertificateLevelIsNotSupported_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("INVALID"); - sessionCertificate.setCertificateLevel("invalid"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.certificateLevel' has unsupported value", ex.getMessage()); - } - - @Test - void validate_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "ADVANCED"); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status response certificate level is lower than requested", ex.getMessage()); - } - - @Test - void validate_expiredCertificateWasReturned() { - var sessionStatus = toSessionStatus(EXPIRED_CERT, "QUALIFIED"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate is invalid", ex.getMessage()); - } - } - - private static SessionStatus toSessionStatus(String certificateChoiceCert, String certificateLevel) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(certificateChoiceCert)); - sessionCertificate.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - return sessionStatus; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionStatus; + +public class CertificateChoiceResponseValidatorTest { + + private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + + CertificateChoiceResponseValidator certificateChoiceResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + } + + @Test + void validate() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_returnedCertificateHigherThanRequested_ok() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_nqCertificate() { + var sessionStatus = toSessionStatus(NQ_SIGNING_CERTIFICATE, "ADVANCED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE), response.getCertificate()); + assertEquals(CertificateLevel.ADVANCED, response.getCertificateLevel()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_requestCertificateLevelNotProvided_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus, null)); + assertEquals("Parameter 'requestedCertificateLevel' is not provided", ex.getMessage()); + } + } + + @Nested + class ValidateEndResult { + + @Test + void validate_sessionResultIsNotProvided_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(new SessionStatus())); + assertEquals("Certificate choice session status field 'result' is missing", ex.getMessage()); + } + + @Test + void validate_sessionEndResultIsNotProvided_throwException() { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(new SessionResult()); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.endResult' is empty", ex.getMessage()); + } + + @Test + void validate_sessionDocumentNumberIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.documentNumber' is empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void validate_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + } + + @Nested + class ValidateCertificate { + + @Test + void validate_sessionCertificateIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.value' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotSupported_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + sessionCertificate.setCertificateLevel("invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } + + @Test + void validate_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status response certificate level is lower than requested", ex.getMessage()); + } + + @Test + void validate_expiredCertificateWasReturned() { + var sessionStatus = toSessionStatus(EXPIRED_CERT, "QUALIFIED"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate is invalid", ex.getMessage()); + } + } + + private static SessionStatus toSessionStatus(String certificateChoiceCert, String certificateLevel) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(certificateChoiceCert)); + sessionCertificate.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateParserTest.java b/src/test/java/ee/sk/smartid/CertificateParserTest.java index 66947565..ae230487 100644 --- a/src/test/java/ee/sk/smartid/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/CertificateParserTest.java @@ -1,41 +1,41 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -public class CertificateParserTest { - - @Test - public void testBothCertificateLevelsQualified() { - assertThrows(SmartIdClientException.class, () -> CertificateParser.parseX509Certificate("invalid")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class CertificateParserTest { + + @Test + public void testBothCertificateLevelsQualified() { + assertThrows(SmartIdClientException.class, () -> CertificateParser.parseX509Certificate("invalid")); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java index dc96b6c7..4fa9a2e3 100644 --- a/src/test/java/ee/sk/smartid/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -1,72 +1,72 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -public final class CertificateUtil { - - private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; - private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; - - private CertificateUtil() { - } - - public static X509Certificate toX509Certificate(byte[] certificateBytes) throws CertificateException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); - } - - public static X509Certificate toX509Certificate(String certificate) { - try { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - public static X509Certificate toX509CertificateFromEncodedString(String base64Certificate) throws CertificateException { - byte[] certificateBytes = getX509CertificateBytes(base64Certificate); - return toX509Certificate(certificateBytes); - } - - public static String getEncodedCertificateData(String certificate) { - return certificate.replace(BEGIN_CERTIFICATE, "") - .replace(END_CERTIFICATE, "") - .replace("\n", ""); - } - - private static byte[] getX509CertificateBytes(String encodedData) { - String certificate = BEGIN_CERTIFICATE + "\n" + encodedData + "\n" + END_CERTIFICATE; - return certificate.getBytes(StandardCharsets.UTF_8); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public final class CertificateUtil { + + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + + private CertificateUtil() { + } + + public static X509Certificate toX509Certificate(byte[] certificateBytes) throws CertificateException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + } + + public static X509Certificate toX509Certificate(String certificate) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + public static X509Certificate toX509CertificateFromEncodedString(String base64Certificate) throws CertificateException { + byte[] certificateBytes = getX509CertificateBytes(base64Certificate); + return toX509Certificate(certificateBytes); + } + + public static String getEncodedCertificateData(String certificate) { + return certificate.replace(BEGIN_CERTIFICATE, "") + .replace(END_CERTIFICATE, "") + .replace("\n", ""); + } + + private static byte[] getX509CertificateBytes(String encodedData) { + String certificate = BEGIN_CERTIFICATE + "\n" + encodedData + "\n" + END_CERTIFICATE; + return certificate.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java index a3e29fa7..f8fe8f0e 100644 --- a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java @@ -1,77 +1,77 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class CertificateValidatorImplTest { - - private static final String TRUSTED_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - private static final String NOT_TRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - - private CertificateValidatorImpl certificateValidator; - - @BeforeEach - void setUp() { - certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build()); - } - - @Test - void validate_ok() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); - - assertDoesNotThrow(() -> certificateValidator.validate(certificate)); - } - - @Test - void validate_expired() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); - assertEquals("Certificate is invalid", exception.getMessage()); - } - - @Test - void validate_notTrusted() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); - assertEquals("Certificate chain validation failed", exception.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class CertificateValidatorImplTest { + + private static final String TRUSTED_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String NOT_TRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private CertificateValidatorImpl certificateValidator; + + @BeforeEach + void setUp() { + certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build()); + } + + @Test + void validate_ok() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + assertDoesNotThrow(() -> certificateValidator.validate(certificate)); + } + + @Test + void validate_expired() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); + assertEquals("Certificate is invalid", exception.getMessage()); + } + + @Test + void validate_notTrusted() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); + assertEquals("Certificate chain validation failed", exception.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java index 8b8590a2..d2dd6f7c 100644 --- a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Map; - -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; -import jakarta.ws.rs.core.MultivaluedMap; - -public class ClientRequestHeaderFilter implements ClientRequestFilter { - - private final Map headersToAdd; - - public ClientRequestHeaderFilter(Map headersToAdd) { - this.headersToAdd = headersToAdd; - } - - @Override - public void filter(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getHeaders(); - for (Map.Entry entry : headersToAdd.entrySet()) { - headers.putSingle(entry.getKey(), entry.getValue()); - } - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Map; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MultivaluedMap; + +public class ClientRequestHeaderFilter implements ClientRequestFilter { + + private final Map headersToAdd; + + public ClientRequestHeaderFilter(Map headersToAdd) { + this.headersToAdd = headersToAdd; + } + + @Override + public void filter(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + for (Map.Entry entry : headersToAdd.entrySet()) { + headers.putSingle(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java index 485f5d81..b52bef7f 100644 --- a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -class DefaultTrustedCAStoreBuilderTest { - - private static final String TRUST_ANCHOR_CERT = FileUtil.readFileToString("test-certs/TEST_SK_ROOT_G1_2021E.pem.crt"); - private static final String INTERMEDIATE_CA_CERT = FileUtil.readFileToString("trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt"); - private static final String OCSP_CERT = FileUtil.readFileToString("test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer"); - - @Test - void buildDefaultTrustedCACertStore_ocspValidationDisabled() { - X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); - TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); - new DefaultTrustedCAStoreBuilder() - .withTrustAnchors(Set.of(trustAnchor)) - .withIntermediateCACertificate(List.of(intermediateCACertificate)) - .withOcspEnabled(false) - .build(); - } - - @Disabled("Fails with OCSP response validation error, needs investigation") - @Test - void buildDefaultTrustedCACertStore_ocspValidationEnabled() { - X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); - TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); - new DefaultTrustedCAStoreBuilder() - .withTrustAnchors(Set.of(trustAnchor)) - .withIntermediateCACertificate(List.of(intermediateCACertificate)) - .withOcspEnabled(true) - .withOCSPValidationCert(CertificateUtil.toX509Certificate(OCSP_CERT)) - .build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class DefaultTrustedCAStoreBuilderTest { + + private static final String TRUST_ANCHOR_CERT = FileUtil.readFileToString("test-certs/TEST_SK_ROOT_G1_2021E.pem.crt"); + private static final String INTERMEDIATE_CA_CERT = FileUtil.readFileToString("trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt"); + private static final String OCSP_CERT = FileUtil.readFileToString("test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer"); + + @Test + void buildDefaultTrustedCACertStore_ocspValidationDisabled() { + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(false) + .build(); + } + + @Disabled("Fails with OCSP response validation error, needs investigation") + @Test + void buildDefaultTrustedCACertStore_ocspValidationEnabled() { + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(true) + .withOCSPValidationCert(CertificateUtil.toX509Certificate(OCSP_CERT)) + .build(); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java index 1f1ba798..d7afcf17 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java @@ -1,267 +1,267 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; -import java.util.Base64; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; - -class DeviceLinkAuthenticationResponseValidatorTest { - - private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - - private DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - } - - @Disabled("Will be fixed when testing with DEMO accounts will be possible") - @Test - void validate_ok() { - String rpChallenge = ""; - SessionStatus sessionStatus = new SessionStatus(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } - - @Disabled - @Test - void validate_qrCodeWasUsedDoNotIncludeInitialCallbackUrlInSignatureValidation_ok() { - // TODO - 26.09.25: implement with demo accounts - } - - @Disabled - @Test - void validate_initialCallbackUrlWasUsed_ok() { - // TODO - 26.09.25: implement with demo accounts - } - - @Disabled("Will be fixed when testing with DEMO accounts will be possible") - @Test - void validate_certificateLevelHigherThanRequested_ok() { - SessionStatus sessionStatus = new SessionStatus(); - SessionCertificate cert = new SessionCertificate(); - cert.setCertificateLevel("QUALIFIED"); - sessionStatus.setCert(cert); - - var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, null, "smart-id-demo", null)); - assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, schemaName, null)); - assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); - } - } - - @Test - void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); - } - - @Nested - class ValidateUserChallenge { - - @ParameterizedTest - @NullAndEmptySource - void validate_sameDeviceFlowButUserChallengeVerifierNotProvided_throwException(String userChallengeVerifier) { - var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", "", "Cjy8feLy_DB1GNF6lLpXf0VbzCMfTaLHzYOOpdXevSc", FlowType.WEB2APP); - - var ex = assertThrows(SmartIdClientException.class, - () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), userChallengeVerifier, "smart-id-demo", null)); - - assertEquals("Parameter 'userChallengeVerifier' must be provided for 'flowType' - WEB2APP", ex.getMessage()); - } - } - - @Nested - class ValidateSessionStatusCertificate { - - @Test - void validate_certificateLevelLowerThanRequested_throwException() { - var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", ""); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var sessionStatus = toSessionStatus(SIGN_CERT, "QUALIFIED", ""); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - } - - @Nested - class ValidateAuthenticationSignature { - - @Test - void validate_invalidSignature_throwException() { - var sessionStatus = toSessionStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Signature value validation failed", ex.getMessage()); - } - } - - private static SessionStatus toSessionStatus(String certificateValue, - String certificateLevel, - String signatureValue) { - return toSessionStatus(certificateValue, certificateLevel, signatureValue, "TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4", FlowType.QR); - } - - private static SessionStatus toSessionStatus(String certificateValue, - String certificateLevel, - String signatureValue, - String userChallengeVerifier, - FlowType flowType) { - var result = new SessionResult(); - result.setEndResult("OK"); - result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - - SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - - var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); - sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); - sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var signature = new SessionSignature(); - signature.setServerRandom(toBase64("a".repeat(43))); - signature.setUserChallenge(userChallengeVerifier); - signature.setValue(toBase64("signatureValue")); - signature.setFlowType(flowType.getDescription()); - signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); - - var cert = new SessionCertificate(); - cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); - cert.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(result); - sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); - sessionStatus.setSignature(signature); - sessionStatus.setCert(cert); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - return sessionStatus; - } - - private static DeviceLinkAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { - return new DeviceLinkAuthenticationSessionRequest( - "00000000-0000-0000-0000-000000000001", - "DEMO", - certificateLevel, - SignatureProtocol.ACSP_V2, - new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - null, - null, - null); - } - - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; + +class DeviceLinkAuthenticationResponseValidatorTest { + + private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + + private DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + } + + @Disabled("Will be fixed when testing with DEMO accounts will be possible") + @Test + void validate_ok() { + String rpChallenge = ""; + SessionStatus sessionStatus = new SessionStatus(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Disabled + @Test + void validate_qrCodeWasUsedDoNotIncludeInitialCallbackUrlInSignatureValidation_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled + @Test + void validate_initialCallbackUrlWasUsed_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled("Will be fixed when testing with DEMO accounts will be possible") + @Test + void validate_certificateLevelHigherThanRequested_ok() { + SessionStatus sessionStatus = new SessionStatus(); + SessionCertificate cert = new SessionCertificate(); + cert.setCertificateLevel("QUALIFIED"); + sessionStatus.setCert(cert); + + var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, null, "smart-id-demo", null)); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, schemaName, null)); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); + } + } + + @Test + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); + } + + @Nested + class ValidateUserChallenge { + + @ParameterizedTest + @NullAndEmptySource + void validate_sameDeviceFlowButUserChallengeVerifierNotProvided_throwException(String userChallengeVerifier) { + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", "", "Cjy8feLy_DB1GNF6lLpXf0VbzCMfTaLHzYOOpdXevSc", FlowType.WEB2APP); + + var ex = assertThrows(SmartIdClientException.class, + () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), userChallengeVerifier, "smart-id-demo", null)); + + assertEquals("Parameter 'userChallengeVerifier' must be provided for 'flowType' - WEB2APP", ex.getMessage()); + } + } + + @Nested + class ValidateSessionStatusCertificate { + + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", ""); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var sessionStatus = toSessionStatus(SIGN_CERT, "QUALIFIED", ""); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + } + + @Nested + class ValidateAuthenticationSignature { + + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Signature value validation failed", ex.getMessage()); + } + } + + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue) { + return toSessionStatus(certificateValue, certificateLevel, signatureValue, "TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4", FlowType.QR); + } + + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue, + String userChallengeVerifier, + FlowType flowType) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom(toBase64("a".repeat(43))); + signature.setUserChallenge(userChallengeVerifier); + signature.setValue(toBase64("signatureValue")); + signature.setFlowType(flowType.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; + } + + private static DeviceLinkAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new DeviceLinkAuthenticationSessionRequest( + "00000000-0000-0000-0000-000000000001", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2, + new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + null, + null, + null); + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index e5e007a2..428bcca5 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -1,481 +1,481 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class DeviceLinkAuthenticationSessionRequestBuilderTest { - - private static final String BASE64_PATTERN = "^[A-Za-z0-9+/]+={0,2}$"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Nested - class ValidateRequiredRequestParameters { - - @Test - void initAuthenticationSession_anonymousAuthentication_ok() throws Exception { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void initAuthenticationSession_withDocumentNumber_ok() { - when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withDocumentNumber("PNOEE-48010010101-MOCK-Q")); - - builder.initAuthenticationSession(); - - ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); - String capturedDocumentNumber = documentNumberCaptor.getValue(); - - assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); - } - - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - builder.initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCertificateLevel(certificateLevel)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - } - - @Test - void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toBaseDeviceLinkRequestBuilder().initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNull(request.requestProperties()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initAuthenticationSession_capabilities_ok(String capabilities) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @Test - void initAuthenticationSession_initialCallbackUrlIsValid_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl("https://example.com/callback")); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("https://example.com/callback", request.initialCallbackUrl()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) - void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_interactionsIsEmpty_throwException(List interactions) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsEmpty_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = - toDeviceLinkRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(duplicateInteractions)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl(url)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals(expectedErrorMessage, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> - b.withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(response); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); - } - } - - @Test - void getAuthenticationSessionRequest_ok() throws Exception { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); - assertEquals("Device link authentication session has not been initialized yet", ex.getMessage()); - } - - private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseDeviceLinkRequestBuilder()); - } - - private DeviceLinkAuthenticationSessionRequestBuilder toBaseDeviceLinkRequestBuilder() { - return new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPin("Log into internet banking system"))); - } - - private DeviceLinkSessionResponse toDeviceLinkAuthenticationResponse() { - return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", - generateBase64String("sessionToken"), - generateBase64String("sessionSecret"), - URI.create("https://example.com/callback")); - } - - private static String generateBase64String(String text) { - return Base64.toBase64String(text.getBytes()); - } - - private void assertAuthenticationSessionRequest(DeviceLinkAuthenticationSessionRequest request) throws Exception { - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals("QUALIFIED", request.certificateLevel()); - assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertNotNull(request.signatureProtocolParameters().rpChallenge()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - - Interaction[] parsed = parseInteractionsFromBase64(request.interactions()); - assertTrue(Stream.of(parsed).anyMatch(i -> i.type().equals("displayTextAndPIN"))); - } - - private Interaction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { - byte[] decodedBytes = Base64.decode(base64EncodedJson); - String json = new String(decodedBytes, StandardCharsets.UTF_8); - var mapper = new ObjectMapper(); - return mapper.readValue(json, Interaction[].class); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class DeviceLinkAuthenticationSessionRequestBuilderTest { + + private static final String BASE64_PATTERN = "^[A-Za-z0-9+/]+={0,2}$"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + void initAuthenticationSession_anonymousAuthentication_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withDocumentNumber("PNOEE-48010010101-MOCK-Q")); + + builder.initAuthenticationSession(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + builder.initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCertificateLevel(certificateLevel)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + } + + @Test + void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toBaseDeviceLinkRequestBuilder().initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNull(request.requestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initAuthenticationSession_capabilities_ok(String capabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @Test + void initAuthenticationSession_initialCallbackUrlIsValid_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl("https://example.com/callback")); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("https://example.com/callback", request.initialCallbackUrl()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_interactionsIsEmpty_throwException(List interactions) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsEmpty_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = + toDeviceLinkRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(duplicateInteractions)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> + b.withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(response); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); + } + } + + @Test + void getAuthenticationSessionRequest_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Device link authentication session has not been initialized yet", ex.getMessage()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseDeviceLinkRequestBuilder()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toBaseDeviceLinkRequestBuilder() { + return new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPin("Log into internet banking system"))); + } + + private DeviceLinkSessionResponse toDeviceLinkAuthenticationResponse() { + return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", + generateBase64String("sessionToken"), + generateBase64String("sessionSecret"), + URI.create("https://example.com/callback")); + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private void assertAuthenticationSessionRequest(DeviceLinkAuthenticationSessionRequest request) throws Exception { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals("QUALIFIED", request.certificateLevel()); + assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertNotNull(request.signatureProtocolParameters().rpChallenge()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + + Interaction[] parsed = parseInteractionsFromBase64(request.interactions()); + assertTrue(Stream.of(parsed).anyMatch(i -> i.type().equals("displayTextAndPIN"))); + } + + private Interaction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { + byte[] decodedBytes = Base64.decode(base64EncodedJson); + String json = new String(decodedBytes, StandardCharsets.UTF_8); + var mapper = new ObjectMapper(); + return mapper.readValue(json, Interaction[].class); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index cdd8f4ec..669c4867 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -1,550 +1,550 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.matchesPattern; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; -import java.util.Map; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class DeviceLinkBuilderTest { - - private static final String SESSION_SECRET = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); - private static final String DEMO_SCHEMA_NAME = "smart-id-demo"; - private static final String DEVICE_LINK_BASE = "https://smart-id.com/device-link/"; - private static final String DEVICE_LINK_HOST = "smart-id.com"; - private static final String SESSION_TOKEN = "token123"; - private static final String LANGUAGE = "eng"; - private static final String VERSION_INVALID = "0.9"; - private static final long ELAPSED_SECONDS = 1L; - private static final String CALLBACK_URL = "https://callback.url"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String BASE64_DIGEST = "dGVzdC1kaWdlc3Q="; - private static final String BROKERED_RP = "QlJP"; - private static final String BASE64_INTERACTIONS = "SW50ZXJhY3Rpb25z"; - private static final String AUTH_CODE_PATTERN = "^[A-Za-z0-9_-]{43}$"; - - @Nested - class CreateUnprotectedUri { - - @ParameterizedTest - @EnumSource - void createUri_validInputs_shouldBuildUri(DeviceLinkType deviceLinkType) { - URI uri = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(deviceLinkType) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(deviceLinkType == DeviceLinkType.QR_CODE ? ELAPSED_SECONDS : null) - .createUnprotectedUri(); - - assertThat(uri.getHost(), equalTo(DEVICE_LINK_HOST)); - } - - @Test - void createUri_invalidVersion_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withVersion(VERSION_INVALID) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Only version 1.0 is allowed", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingDeviceLinkBase_throwsException(String base) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(base) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'deviceLinkBase' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingVersion_throwsException(String version) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withVersion(version) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'version' cannot be empty", ex.getMessage()); - } - - @Test - void createUri_missingDeviceLinkType_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(null) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'deviceLinkType' must be set", ex.getMessage()); - } - - @Test - void createUri_missingSessionType_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'sessionType' must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingSessionToken_throwsException(String token) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(token) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'sessionToken' cannot be empty", ex.getMessage()); - } - - @Test - void createUri_missingElapsedSecondsForQrCode_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .createUnprotectedUri() - ); - assertEquals("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingLang_throwsException(String lang) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(lang) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'lang' must be set", ex.getMessage()); - } - - @Test - void createUri_elapsedSecondsSetForNonQrCode_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE", ex.getMessage()); - } - } - - @Nested - class BuildDeviceLink { - - @ParameterizedTest - @EnumSource(value = SessionType.class) - void buildDeviceLink(SessionType sessionType) { - DeviceLinkBuilder builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(sessionType) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME); - - if (sessionType != SessionType.CERTIFICATE_CHOICE) { - builder.withDigest(BASE64_DIGEST) - .withInteractions(BASE64_INTERACTIONS); - } - - URI uri = builder.buildDeviceLink(SESSION_SECRET); - - Map params = toQueryParamsMap(uri); - assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); - } - - @Test - void buildDeviceLink_withCustomSchemeName() { - String authCode = toQueryParamsMap( - new DeviceLinkBuilder() - .withSchemeName(DEMO_SCHEMA_NAME) - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(BASE64_INTERACTIONS) - .buildDeviceLink(SESSION_SECRET) - ).get("authCode"); - - assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); - } - - @Test - void buildDeviceLink_sameDeviceFlowWithCallback_ok() { - URI uri = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withInitialCallbackUrl(CALLBACK_URL) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET); - - Map params = toQueryParamsMap(uri); - assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingSchemeName_throwsException(String scheme) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withSchemeName(scheme) - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'schemeName' cannot be empty", ex.getMessage()); - } - - @Test - void buildDeviceLink_missingRelyingPartyName_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingDigestForAuthentication_throwsException(String digest) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(digest) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingDigestForSignature_throwsException(String digest) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.SIGNATURE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(digest) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @Test - void buildDeviceLink_certificateChoiceAndDigestIsSet_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withDigest(BASE64_DIGEST) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); - } - - @Test - void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInitialCallbackUrl(CALLBACK_URL) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE", exception.getMessage()); - } - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"APP_2_APP", "WEB_2_APP"}) - void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLinkType deviceLinkType) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(deviceLinkType) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_interactionsMissingForAuthentication_throwsException(String interactions) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(BASE64_DIGEST) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(interactions) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_interactionsMissingForSignature_throwsException(String interactions) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.SIGNATURE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(BASE64_DIGEST) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(interactions) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @Test - void buildDeviceLink_interactionsSetForCertificateChoice_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); - } - - @Test - void buildDeviceLink_invalidBase64Key_shouldThrowException() { - var builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME); - - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); - - assertEquals("Failed to calculate authCode", exception.getMessage()); - assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_sessionSecretIsEmpty_throwException(String sessionSecret) { - var builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME); - - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(sessionSecret)); - - assertEquals("Parameter 'sessionSecret' cannot be empty", exception.getMessage()); - } - } - - private static Map toQueryParamsMap(URI uri) { - return Arrays.stream(uri.getQuery().split("&")) - .map(s -> s.split("=")) - .collect(Collectors.toMap(s -> s[0], s -> s[1])); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class DeviceLinkBuilderTest { + + private static final String SESSION_SECRET = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + private static final String DEMO_SCHEMA_NAME = "smart-id-demo"; + private static final String DEVICE_LINK_BASE = "https://smart-id.com/device-link/"; + private static final String DEVICE_LINK_HOST = "smart-id.com"; + private static final String SESSION_TOKEN = "token123"; + private static final String LANGUAGE = "eng"; + private static final String VERSION_INVALID = "0.9"; + private static final long ELAPSED_SECONDS = 1L; + private static final String CALLBACK_URL = "https://callback.url"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final String BASE64_DIGEST = "dGVzdC1kaWdlc3Q="; + private static final String BROKERED_RP = "QlJP"; + private static final String BASE64_INTERACTIONS = "SW50ZXJhY3Rpb25z"; + private static final String AUTH_CODE_PATTERN = "^[A-Za-z0-9_-]{43}$"; + + @Nested + class CreateUnprotectedUri { + + @ParameterizedTest + @EnumSource + void createUri_validInputs_shouldBuildUri(DeviceLinkType deviceLinkType) { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(deviceLinkType) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(deviceLinkType == DeviceLinkType.QR_CODE ? ELAPSED_SECONDS : null) + .createUnprotectedUri(); + + assertThat(uri.getHost(), equalTo(DEVICE_LINK_HOST)); + } + + @Test + void createUri_invalidVersion_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withVersion(VERSION_INVALID) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Only version 1.0 is allowed", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingDeviceLinkBase_throwsException(String base) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(base) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'deviceLinkBase' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingVersion_throwsException(String version) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withVersion(version) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'version' cannot be empty", ex.getMessage()); + } + + @Test + void createUri_missingDeviceLinkType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(null) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'deviceLinkType' must be set", ex.getMessage()); + } + + @Test + void createUri_missingSessionType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'sessionType' must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingSessionToken_throwsException(String token) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(token) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'sessionToken' cannot be empty", ex.getMessage()); + } + + @Test + void createUri_missingElapsedSecondsForQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .createUnprotectedUri() + ); + assertEquals("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingLang_throwsException(String lang) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(lang) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'lang' must be set", ex.getMessage()); + } + + @Test + void createUri_elapsedSecondsSetForNonQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE", ex.getMessage()); + } + } + + @Nested + class BuildDeviceLink { + + @ParameterizedTest + @EnumSource(value = SessionType.class) + void buildDeviceLink(SessionType sessionType) { + DeviceLinkBuilder builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(sessionType) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME); + + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + builder.withDigest(BASE64_DIGEST) + .withInteractions(BASE64_INTERACTIONS); + } + + URI uri = builder.buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + + @Test + void buildDeviceLink_withCustomSchemeName() { + String authCode = toQueryParamsMap( + new DeviceLinkBuilder() + .withSchemeName(DEMO_SCHEMA_NAME) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(BASE64_INTERACTIONS) + .buildDeviceLink(SESSION_SECRET) + ).get("authCode"); + + assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); + } + + @Test + void buildDeviceLink_sameDeviceFlowWithCallback_ok() { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withInitialCallbackUrl(CALLBACK_URL) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingSchemeName_throwsException(String scheme) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withSchemeName(scheme) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'schemeName' cannot be empty", ex.getMessage()); + } + + @Test + void buildDeviceLink_missingRelyingPartyName_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForAuthentication_throwsException(String digest) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForSignature_throwsException(String digest) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @Test + void buildDeviceLink_certificateChoiceAndDigestIsSet_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDigest(BASE64_DIGEST) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); + } + + @Test + void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInitialCallbackUrl(CALLBACK_URL) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE", exception.getMessage()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"APP_2_APP", "WEB_2_APP"}) + void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLinkType deviceLinkType) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(deviceLinkType) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForAuthentication_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForSignature_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @Test + void buildDeviceLink_interactionsSetForCertificateChoice_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); + } + + @Test + void buildDeviceLink_invalidBase64Key_shouldThrowException() { + var builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); + + assertEquals("Failed to calculate authCode", exception.getMessage()); + assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_sessionSecretIsEmpty_throwException(String sessionSecret) { + var builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(sessionSecret)); + + assertEquals("Parameter 'sessionSecret' cannot be empty", exception.getMessage()); + } + } + + private static Map toQueryParamsMap(URI uri) { + return Arrays.stream(uri.getQuery().split("&")) + .map(s -> s.split("=")) + .collect(Collectors.toMap(s -> s[0], s -> s[1])); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index 91c63e40..38e6a8b0 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -1,278 +1,278 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.util.Set; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; - -class DeviceLinkCertificateChoiceSessionRequestBuilderTest { - - private SmartIdConnector connector; - private DeviceLinkCertificateChoiceSessionRequestBuilder builderService; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - - builderService = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withInitialCallbackUrl("https://example.com/callback"); - } - - @Test - void initiateCertificateChoice() { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullRequestProperties() { - builderService.withShareMdClientIpAddress(false); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_missingCertificateLevel() { - builderService.withCertificateLevel(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initiateCertificateChoice_withValidCapabilities(String[] capabilities, Set expectedCapabilities) { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - builderService.withCapabilities(capabilities).initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkCertificateChoiceSessionRequest.class); - verify(connector).initDeviceLinkCertificateChoice(requestCaptor.capture()); - DeviceLinkCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { - var response = new DeviceLinkSessionResponse(sessionId, - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link"), - null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { - var response = new DeviceLinkSessionResponse("test-session-id", - sessionToken, - "test-session-secret", - URI.create("https://example.com/device-link")); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - sessionSecret, - URI.create("https://example.com/device-link")); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - uriString == null ? null : URI.create(uriString)); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_userAccountNotFound() { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); - - var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); - assertEquals(UserAccountNotFoundException.class, ex.getClass()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyUUID() { - builderService.withRelyingPartyUUID(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyName() { - builderService.withRelyingPartyName(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { - builderService.withNonce(invalidNonce); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'nonce' must have length between 1 and 30 characters", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_withoutInitialCallbackUrl() { - builderService.withInitialCallbackUrl(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullNonce() { - builderService.withNonce(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url) { - var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce("123456") - .withInitialCallbackUrl(url); - - var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); - assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); - } - } - - private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { - return new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com"), - Arguments.of("https://example.com|test"), - Arguments.of("ftp://example.com") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; + +class DeviceLinkCertificateChoiceSessionRequestBuilderTest { + + private SmartIdConnector connector; + private DeviceLinkCertificateChoiceSessionRequestBuilder builderService; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + + builderService = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withInitialCallbackUrl("https://example.com/callback"); + } + + @Test + void initiateCertificateChoice() { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullRequestProperties() { + builderService.withShareMdClientIpAddress(false); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_missingCertificateLevel() { + builderService.withCertificateLevel(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initiateCertificateChoice_withValidCapabilities(String[] capabilities, Set expectedCapabilities) { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + builderService.withCapabilities(capabilities).initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkCertificateChoiceSessionRequest.class); + verify(connector).initDeviceLinkCertificateChoice(requestCaptor.capture()); + DeviceLinkCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { + var response = new DeviceLinkSessionResponse(sessionId, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link"), + null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + uriString == null ? null : URI.create(uriString)); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_userAccountNotFound() { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); + + var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); + assertEquals(UserAccountNotFoundException.class, ex.getClass()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyUUID() { + builderService.withRelyingPartyUUID(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyName() { + builderService.withRelyingPartyName(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { + builderService.withNonce(invalidNonce); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'nonce' must have length between 1 and 30 characters", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_withoutInitialCallbackUrl() { + builderService.withInitialCallbackUrl(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullNonce() { + builderService.withNonce(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url) { + var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce("123456") + .withInitialCallbackUrl(url); + + var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); + } + } + + private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 4d1cb1e2..60b8cd0d 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -1,522 +1,522 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; - -class DeviceLinkSignatureSessionRequestBuilderTest { - - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - assertEquals("test-session-id", signatureSessionResponse.sessionID()); - assertEquals("test-session-token", signatureSessionResponse.sessionToken()); - assertEquals("test-session-secret", signatureSessionResponse.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signatureSessionResponse.deviceLinkBase()); - } - - @Test - void initSignatureSession_withDocumentNumber() { - String documentNumber = "PNOEE-31111111111-MOCK-Q"; - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b - .withSemanticsIdentifier(null) - .withDocumentNumber(documentNumber)); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signature); - assertEquals("test-session-id", signature.sessionID()); - assertEquals("test-session-token", signature.sessionToken()); - assertEquals("test-session-secret", signature.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initSignatureSession_withNonce_ok(String nonce) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - } - - @Test - void initSignatureSession_withRequestProperties() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.requestProperties()); - assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); - } - - @Test - void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); - assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(mockSignatureSessionResponse()); - - DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertEquals("test-session-id", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.capabilities()); - } - - @Test - void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @Test - void getSignatureSessionRequest_ok() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); - assertNotNull(signature); - - assertEquals("test-relying-party-uuid", deviceLinkSignatureSessionRequest.relyingPartyUUID()); - assertEquals("DEMO", deviceLinkSignatureSessionRequest.relyingPartyName()); - assertEquals("RAW_DIGEST_SIGNATURE", deviceLinkSignatureSessionRequest.signatureProtocol()); - assertNotNull(deviceLinkSignatureSessionRequest.signatureProtocolParameters()); - assertNotNull(deviceLinkSignatureSessionRequest.interactions()); - } - - @Test - void getSignatureSessionRequest_sessionNotStarted_throwException() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); - assertEquals("Signature session has not been initiated yet", ex.getMessage()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataWithHashAlgorithmSetToNull_throwsException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData("Test data".getBytes(), null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashWithHashAlgorithmSetToNull_throwsException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash("Test data".getBytes(), null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_whenSignableHashAndDataAreNull_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashBeingSetAfterSignableData_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> toBaseDeviceLinkSessionRequestBuilder() - .withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has already been set with SignableData.", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataBeingSetAfterSignableHash_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) - .withSignableHash(new SignableHash("Test data".getBytes())) - .withSignableData(new SignableData("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has already been set with SignableHash.", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInitialCallbackUrl(url)); - - var exception = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_whenInteractionsIsNullOrEmpty_throwException(List interactions) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_interactionsListWithNullValue_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initSignatureSession_invalidNonce(String nonce) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); - } - } - - @Nested - class ResponseValidationTests { - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionID(String sessionID) { - var response = new DeviceLinkSessionResponse(sessionID, - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionToken(String sessionToken) { - var response = new DeviceLinkSessionResponse("test-session-id", - sessionToken, - "test-session-secret", - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionSecret(String sessionSecret) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - sessionSecret, - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); - } - } - - private DeviceLinkSignatureSessionRequestBuilder toDeviceLinkSignatureSessionRequestBuilder(UnaryOperator builder) { - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - return builder.apply(deviceLinkSessionRequestBuilder); - } - - private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestBuilder() { - return new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document"))) - .withSignableData(new SignableData("Test data".getBytes())); - } - - private DeviceLinkSessionResponse mockSignatureSessionResponse() { - return new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, null), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com"), - Arguments.of("https://example.com|test"), - Arguments.of("ftp://example.com") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; + +class DeviceLinkSignatureSessionRequestBuilderTest { + + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + assertEquals("test-session-id", signatureSessionResponse.sessionID()); + assertEquals("test-session-token", signatureSessionResponse.sessionToken()); + assertEquals("test-session-secret", signatureSessionResponse.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signatureSessionResponse.deviceLinkBase()); + } + + @Test + void initSignatureSession_withDocumentNumber() { + String documentNumber = "PNOEE-31111111111-MOCK-Q"; + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b + .withSemanticsIdentifier(null) + .withDocumentNumber(documentNumber)); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.sessionID()); + assertEquals("test-session-token", signature.sessionToken()); + assertEquals("test-session-secret", signature.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + } + + @Test + void initSignatureSession_withRequestProperties() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.requestProperties()); + assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); + } + + @Test + void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); + assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockSignatureSessionResponse()); + + DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); + assertEquals("test-session-id", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + } + + @Test + void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @Test + void getSignatureSessionRequest_ok() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); + assertNotNull(signature); + + assertEquals("test-relying-party-uuid", deviceLinkSignatureSessionRequest.relyingPartyUUID()); + assertEquals("DEMO", deviceLinkSignatureSessionRequest.relyingPartyName()); + assertEquals("RAW_DIGEST_SIGNATURE", deviceLinkSignatureSessionRequest.signatureProtocol()); + assertNotNull(deviceLinkSignatureSessionRequest.signatureProtocolParameters()); + assertNotNull(deviceLinkSignatureSessionRequest.interactions()); + } + + @Test + void getSignatureSessionRequest_sessionNotStarted_throwException() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); + assertEquals("Signature session has not been initiated yet", ex.getMessage()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableHashAndDataAreNull_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashBeingSetAfterSignableData_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> toBaseDeviceLinkSessionRequestBuilder() + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableData.", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataBeingSetAfterSignableHash_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withSignableHash(new SignableHash("Test data".getBytes())) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableHash.", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_whenInteractionsIsNullOrEmpty_throwException(List interactions) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_interactionsListWithNullValue_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionID(String sessionID) { + var response = new DeviceLinkSessionResponse(sessionID, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionToken(String sessionToken) { + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionSecret(String sessionSecret) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); + } + } + + private DeviceLinkSignatureSessionRequestBuilder toDeviceLinkSignatureSessionRequestBuilder(UnaryOperator builder) { + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + return builder.apply(deviceLinkSessionRequestBuilder); + } + + private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestBuilder() { + return new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document"))) + .withSignableData(new SignableData("Test data".getBytes())); + } + + private DeviceLinkSessionResponse mockSignatureSessionResponse() { + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index aa6ad3f0..f997fafb 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -1,79 +1,79 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.stream.Stream; - -import org.apache.commons.codec.binary.Hex; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -public class DigestCalculatorTest { - - private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); - - @ParameterizedTest - @ArgumentsSource(DigestAlgorithmValueProvider.class) - public void calculateDigest_sha256(HashAlgorithm hashAlgorithm, String expectedHex) { - byte[] sha = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, hashAlgorithm); - - assertThat(Hex.encodeHexString(sha), is(expectedHex)); - } - - @Test - public void calculateDigest_nullHashType() { - var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - private static class DigestAlgorithmValueProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(HashAlgorithm.SHA_256, "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"), - Arguments.of(HashAlgorithm.SHA_384, "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a"), - Arguments.of(HashAlgorithm.SHA_512, "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"), - Arguments.of(HashAlgorithm.SHA3_256, "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af"), - Arguments.of(HashAlgorithm.SHA3_384, "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c"), - Arguments.of(HashAlgorithm.SHA3_512, "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.apache.commons.codec.binary.Hex; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class DigestCalculatorTest { + + private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); + + @ParameterizedTest + @ArgumentsSource(DigestAlgorithmValueProvider.class) + public void calculateDigest_sha256(HashAlgorithm hashAlgorithm, String expectedHex) { + byte[] sha = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, hashAlgorithm); + + assertThat(Hex.encodeHexString(sha), is(expectedHex)); + } + + @Test + public void calculateDigest_nullHashType() { + var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + private static class DigestAlgorithmValueProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(HashAlgorithm.SHA_256, "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"), + Arguments.of(HashAlgorithm.SHA_384, "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a"), + Arguments.of(HashAlgorithm.SHA_512, "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"), + Arguments.of(HashAlgorithm.SHA3_256, "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af"), + Arguments.of(HashAlgorithm.SHA3_384, "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c"), + Arguments.of(HashAlgorithm.SHA3_512, "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java index 36285640..a9fa46a7 100644 --- a/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java +++ b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; - -public class DuplicateDeviceLinkInteractionsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - var interaction1 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); - var interaction2 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); - - return Stream.of( - Arguments.of(List.of(interaction1, interaction1)), - Arguments.of(List.of(interaction1, interaction2)) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; + +public class DuplicateDeviceLinkInteractionsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + var interaction1 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + var interaction2 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + + return Stream.of( + Arguments.of(List.of(interaction1, interaction1)), + Arguments.of(List.of(interaction1, interaction2)) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java index b3d305cd..f55a85c8 100644 --- a/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java @@ -1,49 +1,49 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; - -public class DuplicateNotificationInteractionArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - List.of(NotificationInteraction.displayTextAndPin("Enter your PIN."), - NotificationInteraction.displayTextAndPin("Enter your PIN.")), - List.of(NotificationInteraction.displayTextAndPin("Provide your PIN"), - NotificationInteraction.displayTextAndPin("Enter your PIN."))) - .map(Arguments::of); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; + +public class DuplicateNotificationInteractionArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + List.of(NotificationInteraction.displayTextAndPin("Enter your PIN."), + NotificationInteraction.displayTextAndPin("Enter your PIN.")), + List.of(NotificationInteraction.displayTextAndPin("Provide your PIN"), + NotificationInteraction.displayTextAndPin("Enter your PIN."))) + .map(Arguments::of); + } +} diff --git a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java index 4e48ca24..daf9906e 100644 --- a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java +++ b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java @@ -1,131 +1,131 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionStatus; - -class ErrorResultHandlerTest { - - @Test - void handle_nullInput() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); - assertEquals("Parameter 'sessionResult' is not provided", smartIdClientException.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void handle_notOKEndResults(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionResult)); - } - - @ParameterizedTest - @ValueSource(strings = {"", "UNKNOWN"}) - void handle_unknownEndResult(String unknownEndResult) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(unknownEndResult); - - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(sessionResult)); - assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); - } - - @Test - void handle_endResultIsUserRefusedInteraction_detailsMissing() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Details for refused interaction are missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_endResultIsUserRefusedInteraction_interactionIsEmpty(String interaction) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Details for refused interaction are missing", exception.getMessage()); - } - - @Test - void handle_endResultIsUserRefusedInteraction_interactionIsInvalidValue() { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction("invalid interaction"); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Unexpected interaction type: invalid interaction", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void handle_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionStatus; + +class ErrorResultHandlerTest { + + @Test + void handle_nullInput() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); + assertEquals("Parameter 'sessionResult' is not provided", smartIdClientException.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void handle_notOKEndResults(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionResult)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "UNKNOWN"}) + void handle_unknownEndResult(String unknownEndResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(unknownEndResult); + + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(sessionResult)); + assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); + } + + @Test + void handle_endResultIsUserRefusedInteraction_detailsMissing() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsUserRefusedInteraction_interactionIsEmpty(String interaction) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @Test + void handle_endResultIsUserRefusedInteraction_interactionIsInvalidValue() { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction("invalid interaction"); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Unexpected interaction type: invalid interaction", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void handle_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + } +} diff --git a/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java index a7395253..afbbea9b 100644 --- a/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java +++ b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java @@ -1,102 +1,102 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class FileDefaultTrustedCAStoreBuilderTest { - - @Test - void validateTrustedCaCertificatesOnInitiation_ocspValidationsDisabled() { - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - assertFalse(trustedCACertStore.getTrustedCACertificates().isEmpty()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPathIsSetToEmpty_throwException(String path) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withTrustAnchorTruststorePath(path) - .build(); - }); - assertEquals("Trust anchor truststore path must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPasswordIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withTrustAnchorTruststorePassword(password) - .build(); - }); - assertEquals("Trust anchor truststore password must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePathIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withIntermediateCATruststorePath(password) - .build(); - }); - assertEquals("Intermediate CA certificate truststore path must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePasswordIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withIntermediateCATruststorePassword(password) - .build(); - }); - assertEquals("Intermediate CA certificate truststore password must be set", ex.getMessage()); - } - - @Disabled("Not yet implemented") - @Test - void validateTrustedCaCertificatesOnInitiation_withOCSPValidationTurnedOn() { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(true).build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class FileDefaultTrustedCAStoreBuilderTest { + + @Test + void validateTrustedCaCertificatesOnInitiation_ocspValidationsDisabled() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + assertFalse(trustedCACertStore.getTrustedCACertificates().isEmpty()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPathIsSetToEmpty_throwException(String path) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePath(path) + .build(); + }); + assertEquals("Trust anchor truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePassword(password) + .build(); + }); + assertEquals("Trust anchor truststore password must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePathIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePath(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePassword(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore password must be set", ex.getMessage()); + } + + @Disabled("Not yet implemented") + @Test + void validateTrustedCaCertificatesOnInitiation_withOCSPValidationTurnedOn() { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(true).build(); + } +} diff --git a/src/test/java/ee/sk/smartid/FileUtil.java b/src/test/java/ee/sk/smartid/FileUtil.java index 20ad20ae..c32d7b5d 100644 --- a/src/test/java/ee/sk/smartid/FileUtil.java +++ b/src/test/java/ee/sk/smartid/FileUtil.java @@ -1,55 +1,55 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; - -public final class FileUtil { - - private FileUtil() { - } - - public static String readFileToString(String fileName) { - return new String(readFileBytes(fileName), StandardCharsets.UTF_8); - } - - public static byte[] readFileBytes(String fileName) { - try { - ClassLoader classLoader = FileUtil.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull(resource, "File not found: " + fileName); - return Files.readAllBytes(Paths.get(resource.toURI())); - } catch (Exception e) { - throw new RuntimeException("Exception: " + e.getMessage(), e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public final class FileUtil { + + private FileUtil() { + } + + public static String readFileToString(String fileName) { + return new String(readFileBytes(fileName), StandardCharsets.UTF_8); + } + + public static byte[] readFileBytes(String fileName) { + try { + ClassLoader classLoader = FileUtil.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + return Files.readAllBytes(Paths.get(resource.toURI())); + } catch (Exception e) { + throw new RuntimeException("Exception: " + e.getMessage(), e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java index e3fc3a35..737a141d 100644 --- a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java +++ b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java @@ -1,146 +1,146 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.math.BigInteger; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; -import java.security.SignatureException; -import java.security.cert.X509Certificate; -import java.util.Date; - -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.bouncycastle.jce.X509Principal; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.x509.X509V3CertificateGenerator; - -public final class InvalidCertificateGenerator { - - private InvalidCertificateGenerator() { - } - - public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { - ASN1EncodableVector vec = new ASN1EncodableVector(); - vec.addAll(policyInformations); - return CertificatePolicies.getInstance(new DERSequence(vec)); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private CertificatePolicies policies; - private ExtendedKeyUsage extendedKeyUsage; - private KeyUsage keyUsage; - private QCStatement qcStatement; - - public Builder withPolicies(CertificatePolicies policies) { - this.policies = policies; - return this; - } - - public Builder withExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) { - this.extendedKeyUsage = extendedKeyUsage; - return this; - } - - public Builder withKeyUsage(KeyUsage keyUsage) { - this.keyUsage = keyUsage; - return this; - } - - public Builder withQcStatement(QCStatement qcStatement) { - this.qcStatement = qcStatement; - return this; - } - - public X509Certificate createCertificate() { - Security.addProvider(new BouncyCastleProvider()); - KeyPair kp = createKeyPair(); - X509V3CertificateGenerator certGen = getBaseX509Generator(kp); - if (policies != null) { - certGen.addExtension(Extension.certificatePolicies, false, policies); - } - if (extendedKeyUsage != null) { - certGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); - } - if (keyUsage != null) { - certGen.addExtension(Extension.keyUsage, true, keyUsage); - } - if (qcStatement != null) { - certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); - } - return generate(certGen, kp); - } - - private static KeyPair createKeyPair() { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); - kpg.initialize(2048); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new RuntimeException(e); - } - } - - private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { - X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); - X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); - - X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setIssuerDN(issuer); - certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); - certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); - certGen.setSubjectDN(subject); - certGen.setPublicKey(kp.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen; - } - - private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { - try { - return certGen.generateX509Certificate(kp.getPrivate(), "BC"); - } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { - throw new RuntimeException(e); - } - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +public final class InvalidCertificateGenerator { + + private InvalidCertificateGenerator() { + } + + public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { + ASN1EncodableVector vec = new ASN1EncodableVector(); + vec.addAll(policyInformations); + return CertificatePolicies.getInstance(new DERSequence(vec)); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private CertificatePolicies policies; + private ExtendedKeyUsage extendedKeyUsage; + private KeyUsage keyUsage; + private QCStatement qcStatement; + + public Builder withPolicies(CertificatePolicies policies) { + this.policies = policies; + return this; + } + + public Builder withExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) { + this.extendedKeyUsage = extendedKeyUsage; + return this; + } + + public Builder withKeyUsage(KeyUsage keyUsage) { + this.keyUsage = keyUsage; + return this; + } + + public Builder withQcStatement(QCStatement qcStatement) { + this.qcStatement = qcStatement; + return this; + } + + public X509Certificate createCertificate() { + Security.addProvider(new BouncyCastleProvider()); + KeyPair kp = createKeyPair(); + X509V3CertificateGenerator certGen = getBaseX509Generator(kp); + if (policies != null) { + certGen.addExtension(Extension.certificatePolicies, false, policies); + } + if (extendedKeyUsage != null) { + certGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); + } + if (keyUsage != null) { + certGen.addExtension(Extension.keyUsage, true, keyUsage); + } + if (qcStatement != null) { + certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); + } + return generate(certGen, kp); + } + + private static KeyPair createKeyPair() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { + X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); + X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); + + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setIssuerDN(issuer); + certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); + certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); + certGen.setSubjectDN(subject); + certGen.setPublicKey(kp.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return certGen; + } + + private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { + try { + return certGen.generateX509Certificate(kp.getPrivate(), "BC"); + } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java index fabc18c2..dec1f549 100644 --- a/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.bouncycastle.util.encoders.Base64.*; - -import java.util.stream.Stream; - -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -public class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Value for 'rpChallenge' must be Base64-encoded string"), - Arguments.of(Named.of("provided value sizes is less than allowed", toBase64String("a".repeat(30).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters"), - Arguments.of(Named.of("provided value sizes exceeds max range value", toBase64String("a".repeat(67).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters") - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.bouncycastle.util.encoders.Base64.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Value for 'rpChallenge' must be Base64-encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", toBase64String("a".repeat(30).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters"), + Arguments.of(Named.of("provided value sizes exceeds max range value", toBase64String("a".repeat(67).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters") + ); + } +} diff --git a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java index 2e817836..8de5f459 100644 --- a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -1,258 +1,258 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; - -class LinkedNotificationSignatureSessionRequestBuilderTest { - - private static final String DOCUMENT_NUMBER = "PNOEE-12345678901-MOCK-Q"; - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_ok() { - LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @ParameterizedTest - @EnumSource(CertificateLevel.class) - void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel certificateLevel) { - LinkedNotificationSignatureSessionRequestBuilder builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(certificateLevel) - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); - verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - LinkedSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { - LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); - verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - LinkedSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedRequestCapabilities, request.capabilities()); - } - - @Nested - class ValidateRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_documentNumberIsEmpty_throwException(String documentNumber) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataOrSignableHashNotProvided_throwException() { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with SignableData or with SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAlreadyUsedForSettingDigest_throwException() { - var builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> builder.withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512)))); - assertEquals("Value for 'digestInput' has been already set with SignableData", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException() { - var builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber(DOCUMENT_NUMBER); - - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> builder.withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSignableData(new SignableData("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withLinkedSessionID(linkedSessionID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'linkedSessionID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"1234567890123456789012345678901", ""}) - void initSignatureSession_nonceWithIncorrectLengthProvided_throwException(String nonce) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'nonce' must be 1-30 characters long", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_interactionsInEmpty_throwException(List interactions) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initSignatureSession_interactionsContainDuplicates_throwException(List interactions) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> - b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - } - - @Test - void initSignatureSession_sessionIDMissingFromResponse_throwException() { - LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse(null)); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Linked notification-base signature session response field 'sessionID' is missing or empty", ex.getMessage()); - } - - private LinkedNotificationSignatureSessionRequestBuilder toLinkedNotificationSignatureSessionRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseLinkedNotificationSignatureSessionRequestBuilder()); - } - - private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificationSignatureSessionRequestBuilder() { - return new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; + +class LinkedNotificationSignatureSessionRequestBuilderTest { + + private static final String DOCUMENT_NUMBER = "PNOEE-12345678901-MOCK-Q"; + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_ok() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @ParameterizedTest + @EnumSource(CertificateLevel.class) + void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel certificateLevel) { + LinkedNotificationSignatureSessionRequestBuilder builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + + @Nested + class ValidateRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_documentNumberIsEmpty_throwException(String documentNumber) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataOrSignableHashNotProvided_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with SignableData or with SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAlreadyUsedForSettingDigest_throwException() { + var builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512)))); + assertEquals("Value for 'digestInput' has been already set with SignableData", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException() { + var builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withLinkedSessionID(linkedSessionID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'linkedSessionID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"1234567890123456789012345678901", ""}) + void initSignatureSession_nonceWithIncorrectLengthProvided_throwException(String nonce) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'nonce' must be 1-30 characters long", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_interactionsInEmpty_throwException(List interactions) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initSignatureSession_interactionsContainDuplicates_throwException(List interactions) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> + b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + } + + @Test + void initSignatureSession_sessionIDMissingFromResponse_throwException() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse(null)); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Linked notification-base signature session response field 'sessionID' is missing or empty", ex.getMessage()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toLinkedNotificationSignatureSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseLinkedNotificationSignatureSessionRequestBuilder()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificationSignatureSessionRequestBuilder() { + return new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); + } +} diff --git a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java index 26622f2c..d9345579 100644 --- a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java @@ -1,120 +1,120 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.Security; -import java.security.cert.X509Certificate; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class NonQualifiedSignatureCertificatePurposeValidatorTest { - - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; - private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; - - private NonQualifiedSignatureCertificatePurposeValidator validator; - - @BeforeEach - void setUp() { - Security.addProvider(new BouncyCastleProvider()); - validator = new NonQualifiedSignatureCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) - void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { - PolicyInformation skNQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_POLICY_OID), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); - } - - private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class NonQualifiedSignatureCertificatePurposeValidatorTest { + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; + + private NonQualifiedSignatureCertificatePurposeValidator validator; + + @BeforeEach + void setUp() { + Security.addProvider(new BouncyCastleProvider()); + validator = new NonQualifiedSignatureCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skNQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java index 8ae7b76f..52c34e1f 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java @@ -1,204 +1,204 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; - -class NotificationAuthenticationResponseValidatorTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt"); - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - private static final String SIGNATURE_VALUE = "DR6pERkYxg5+pa7c0675yEmithtHzEnsqMyOD7RgZlwJgyR/z7VBxOZOUxdakjkT2LK6Jfo3RqxMeYciGfidieJ6vbdyzLDoSnrreaJguo1W5n5blz6Zqb+bkum/30qex7S31ubmRnNM/yIIVJ+/uuAgZgoQIwUV/KmTOE+0GEWFbqvxqFr7BkfrMX4luRrzfXkzpiWqlO8DXoQq4zfo3c00JsvCiM7PSfK4TLVR1FldXmGiV4ftcIep+YoPIxzzIbToyZ0+XYLIgobBio3EHyp2Z3rEWjASfY7+27c0TLkx8gRchxUcowxepioS49lz0trhMzbxNe1NCskHUAa3oodIH0xPNVD/B03uEKziK0r8mGWanHFvOhlqxnCfeN3AuQi5BJ0X7oybMWEvJ06dHlRBc3LrKhM1RrKkSiMy/eI0lTXDajJPupp7Zq/Ck41GbFnn52woFwYAB0hP2kUf7patya9C5C4QyeWB7SnRqtWTXprOMlPHG/KAjh7d61BhjV94zrFKj6YHcDxoQ6a31laYuyhkPMhqdzui1E/4BhWNiJsMkiqdB++VEgL5eT/76xHQuHIUD4GXHmAJnsQjBjFx5ws/yl5pFWsc/GR5H5oNT73Iaw2WSPReXLr7ZD8XEWmTV/GhjXoRUoEjtJrEIv30dYjXqE9Kv+B89tVk2gPHutgNuJJwwoZUaP61ym9w3WawR7ElJ3A8lvYjBPPOY3nYK/hu10imk/9cjdBJaNnMAlfsyzaXtBwBqdu5d80ibFAXkQ9aLwkqURX/Xnmw+lXIzj+p4T2BzhaGR7994qCVksoWPP/0xdvO+lYDM0YLPTvZTXN2PZVgt9NqYTEZHG6/4bcGoIkDTutAxF859rHBplzlMOGDz+sZPKHnLrKMnWaSaSbCVHi7pwF2vcq6QxkzY0grRAKYmmObPP7ORhIjXt5ENoW6n5CptgowizS4CckiaAe0u3QtMp+NoGYg/LSeef7NFhDDf8tUK0azHlAUDb3HPGUtQ3dvYX3JlCoX"; - - private NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - notificationAuthenticationResponseValidator = NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - } - - @Test - void validate_ok() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", SIGNATURE_VALUE); - - AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); - assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); - assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); - } - } - - @Test - void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); - } - - @Nested - class ValidateSessionStatusCertificate { - - @Test - void validate_certificateLevelLowerThanRequested_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", SIGNATURE_VALUE); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", SIGNATURE_VALUE); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - } - - @Nested - class ValidateAuthenticationSignature { - - @Test - void validate_invalidSignature_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Signature value validation failed", ex.getMessage()); - } - } - - private static NotificationAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { - return new NotificationAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel, - SignatureProtocol.ACSP_V2.name(), - new AcspV2SignatureProtocolParameters("3mhDkd0ulDR/WVZx678FcrNw4pUhrZxcQsmejf8jQ1HtSp3GAxCH/Fi9EEiuULp44G/KNKONPXZELqCSZw4AoA==", - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), - "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB3aXRoIFNtYXJ0LUlEIGRlbW8/In1d", - null, - null, - "numeric4"); - } - - private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { - var result = new SessionResult(); - result.setEndResult("OK"); - result.setDocumentNumber("PNOEE-40504040001-DEMO-Q"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - - SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - - var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); - sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); - sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var signature = new SessionSignature(); - signature.setServerRandom("9eZeWMTJ9YYBtjj5jK8p1sLm"); - signature.setUserChallenge("RvrVNS1GJYCsuEnEqPCdHHn5vl65F3XiBjmxB4zSosw"); - signature.setValue(signatureValue); - signature.setFlowType(FlowType.NOTIFICATION.getDescription()); - signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); - - var cert = new SessionCertificate(); - cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); - cert.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(result); - sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); - sessionStatus.setSignature(signature); - sessionStatus.setCert(cert); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - return sessionStatus; - } - - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; + +class NotificationAuthenticationResponseValidatorTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + private static final String SIGNATURE_VALUE = "DR6pERkYxg5+pa7c0675yEmithtHzEnsqMyOD7RgZlwJgyR/z7VBxOZOUxdakjkT2LK6Jfo3RqxMeYciGfidieJ6vbdyzLDoSnrreaJguo1W5n5blz6Zqb+bkum/30qex7S31ubmRnNM/yIIVJ+/uuAgZgoQIwUV/KmTOE+0GEWFbqvxqFr7BkfrMX4luRrzfXkzpiWqlO8DXoQq4zfo3c00JsvCiM7PSfK4TLVR1FldXmGiV4ftcIep+YoPIxzzIbToyZ0+XYLIgobBio3EHyp2Z3rEWjASfY7+27c0TLkx8gRchxUcowxepioS49lz0trhMzbxNe1NCskHUAa3oodIH0xPNVD/B03uEKziK0r8mGWanHFvOhlqxnCfeN3AuQi5BJ0X7oybMWEvJ06dHlRBc3LrKhM1RrKkSiMy/eI0lTXDajJPupp7Zq/Ck41GbFnn52woFwYAB0hP2kUf7patya9C5C4QyeWB7SnRqtWTXprOMlPHG/KAjh7d61BhjV94zrFKj6YHcDxoQ6a31laYuyhkPMhqdzui1E/4BhWNiJsMkiqdB++VEgL5eT/76xHQuHIUD4GXHmAJnsQjBjFx5ws/yl5pFWsc/GR5H5oNT73Iaw2WSPReXLr7ZD8XEWmTV/GhjXoRUoEjtJrEIv30dYjXqE9Kv+B89tVk2gPHutgNuJJwwoZUaP61ym9w3WawR7ElJ3A8lvYjBPPOY3nYK/hu10imk/9cjdBJaNnMAlfsyzaXtBwBqdu5d80ibFAXkQ9aLwkqURX/Xnmw+lXIzj+p4T2BzhaGR7994qCVksoWPP/0xdvO+lYDM0YLPTvZTXN2PZVgt9NqYTEZHG6/4bcGoIkDTutAxF859rHBplzlMOGDz+sZPKHnLrKMnWaSaSbCVHi7pwF2vcq6QxkzY0grRAKYmmObPP7ORhIjXt5ENoW6n5CptgowizS4CckiaAe0u3QtMp+NoGYg/LSeef7NFhDDf8tUK0azHlAUDb3HPGUtQ3dvYX3JlCoX"; + + private NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + notificationAuthenticationResponseValidator = NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + } + + @Test + void validate_ok() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", SIGNATURE_VALUE); + + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); + } + } + + @Test + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); + } + + @Nested + class ValidateSessionStatusCertificate { + + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", SIGNATURE_VALUE); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", SIGNATURE_VALUE); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + } + + @Nested + class ValidateAuthenticationSignature { + + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signature value validation failed", ex.getMessage()); + } + } + + private static NotificationAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new NotificationAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2.name(), + new AcspV2SignatureProtocolParameters("3mhDkd0ulDR/WVZx678FcrNw4pUhrZxcQsmejf8jQ1HtSp3GAxCH/Fi9EEiuULp44G/KNKONPXZELqCSZw4AoA==", + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB3aXRoIFNtYXJ0LUlEIGRlbW8/In1d", + null, + null, + "numeric4"); + } + + private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-40504040001-DEMO-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom("9eZeWMTJ9YYBtjj5jK8p1sLm"); + signature.setUserChallenge("RvrVNS1GJYCsuEnEqPCdHHn5vl65F3XiBjmxB4zSosw"); + signature.setValue(signatureValue); + signature.setFlowType(FlowType.NOTIFICATION.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index dab26384..086f4627 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -1,385 +1,385 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class NotificationAuthenticationSessionRequestBuilderTest { - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initAuthenticationSession_withDocumentNumber_ok() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder( - b -> b.withDocumentNumber(null).withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - builder.initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingProvided_ok(boolean ipRequested) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_hashAlgorithm_ok(HashAlgorithm expectedHashAlgorithm) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(expectedHashAlgorithm)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedHashAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toNotificationAuthenticationResponse()); - - NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toNotificationAuthenticationResponse()); - - NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedRequestCapabilities, request.capabilities()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) - void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_hashAlgorithmIsSetToNull_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_interactionsAreEmpty_throwException(List interactions) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) - void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); - } - - @Test - void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_documentNumberAndSemanticIdentifierAreBothProvided_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder( - b -> b.withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(sessionId); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Notification-based authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); - } - } - - @Test - void getAuthenticationSessionRequest_ok() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - builder.initAuthenticationSession(); - NotificationAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); - assertEquals("Notification-based authentication session has not been initialized yet", ex.getMessage()); - } - - private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); - } - - private NotificationAuthenticationSessionRequestBuilder toBaseNotificationAuthenticationSessionRequestBuilder() { - return new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withInteractions(Collections.singletonList(NotificationInteraction.displayTextAndPin("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q"); - } - - private NotificationAuthenticationSessionResponse toNotificationAuthenticationResponse() { - return new NotificationAuthenticationSessionResponse("00000000-0000-0000-0000-000000000000"); - } - - private static String generateBase64String(String text) { - return Base64.toBase64String(text.getBytes()); - } - - private static void assertAuthenticationSessionRequest(NotificationAuthenticationSessionRequest request) { - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class NotificationAuthenticationSessionRequestBuilderTest { + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber(null).withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + builder.initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingProvided_ok(boolean ipRequested) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_hashAlgorithm_ok(HashAlgorithm expectedHashAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(expectedHashAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedHashAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_hashAlgorithmIsSetToNull_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_interactionsAreEmpty_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); + } + + @Test + void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_documentNumberAndSemanticIdentifierAreBothProvided_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(sessionId); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Notification-based authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); + } + } + + @Test + void getAuthenticationSessionRequest_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + builder.initAuthenticationSession(); + NotificationAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Notification-based authentication session has not been initialized yet", ex.getMessage()); + } + + private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); + } + + private NotificationAuthenticationSessionRequestBuilder toBaseNotificationAuthenticationSessionRequestBuilder() { + return new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withInteractions(Collections.singletonList(NotificationInteraction.displayTextAndPin("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q"); + } + + private NotificationAuthenticationSessionResponse toNotificationAuthenticationResponse() { + return new NotificationAuthenticationSessionResponse("00000000-0000-0000-0000-000000000000"); + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private static void assertAuthenticationSessionRequest(NotificationAuthenticationSessionRequest request) { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index 94e9cb1c..d8edb254 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -1,270 +1,270 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class NotificationCertificateChoiceSessionRequestBuilderTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initCertificateChoiceSession_withSemanticsIdentifier_ok() { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toBaseNotificationCertChoiceRequestBuilder() - .initCertificateChoice(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedCertificateLevel) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCertificateLevel, request.certificateLevel()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initCertificateChoiceSession_nonce_ok(String nonce) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withNonce(nonce)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - } - - @Test - void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toBaseNotificationCertChoiceRequestBuilder().initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertNull(request.requestProperties()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initCertificateChoiceSession_ipQueryingSet_ok(boolean ipRequested) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withCapabilities(capabilities)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @ParameterizedTest - @NullAndEmptySource - void initCertificateChoiceSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidNonceProvider.class) - void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withNonce(invalidNonce)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initCertificateChoiceSession_semanticsIdentifierMissing_throwException() { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withSemanticsIdentifier(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'semanticIdentifier' must be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(sessionId); - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); - NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initCertificateChoice); - assertEquals("Notification-based certificate choice response field 'sessionID' is missing or empty", exception.getMessage()); - } - } - - private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { - return new NotificationCertificateChoiceSessionResponse("00000000-0000-0000-0000-000000000000"); - } - - private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { - return modifier.apply(toBaseNotificationCertChoiceRequestBuilder()); - } - - private NotificationCertificateChoiceSessionRequestBuilder toBaseNotificationCertChoiceRequestBuilder() { - return new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), - Arguments.of(CertificateLevel.QSCD, "QSCD") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } - - private static class InvalidNonceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Empty string as value", ""), "Value for 'nonce' length must be between 1 and 30 characters"), - Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Value for 'nonce' length must be between 1 and 30 characters") - ); - } - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class NotificationCertificateChoiceSessionRequestBuilderTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initCertificateChoiceSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toBaseNotificationCertChoiceRequestBuilder() + .initCertificateChoice(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedCertificateLevel) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCertificateLevel, request.certificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initCertificateChoiceSession_nonce_ok(String nonce) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(nonce)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + } + + @Test + void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toBaseNotificationCertChoiceRequestBuilder().initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNull(request.requestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initCertificateChoiceSession_ipQueryingSet_ok(boolean ipRequested) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withCapabilities(capabilities)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidNonceProvider.class) + void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(invalidNonce)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initCertificateChoiceSession_semanticsIdentifierMissing_throwException() { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'semanticIdentifier' must be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(sessionId); + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); + NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initCertificateChoice); + assertEquals("Notification-based certificate choice response field 'sessionID' is missing or empty", exception.getMessage()); + } + } + + private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { + return new NotificationCertificateChoiceSessionResponse("00000000-0000-0000-0000-000000000000"); + } + + private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationCertChoiceRequestBuilder()); + } + + private NotificationCertificateChoiceSessionRequestBuilder toBaseNotificationCertChoiceRequestBuilder() { + return new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class InvalidNonceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Empty string as value", ""), "Value for 'nonce' length must be between 1 and 30 characters"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Value for 'nonce' length must be between 1 and 30 characters") + ); + } + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index 55bbf052..a785f1ef 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -1,496 +1,496 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.VerificationCode; - -class NotificationSignatureSessionRequestBuilderTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - private static final String DOCUMENT_NUMBER = "PNOEE-31111111111"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_withSemanticsIdentifier_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = toBaseNotificationSignatureSessionRequestBuilder().initSignatureSession(); - - assertSessionResponse(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(SEMANTICS_IDENTIFIER)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); - } - - @Test - void initSignatureSession_withDocumentNumber_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = toNotificationSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(null).withDocumentNumber(DOCUMENT_NUMBER)) - .initSignatureSession(); - - assertSessionResponse(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initSignatureSession_withCertificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initSignatureSession_withNonce_ok(String nonce) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initSignatureSession_withRequestProperties_ok(boolean shareIp) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(shareIp)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.requestProperties()); - assertEquals(shareIp, capturedRequest.requestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Test - void initSignatureSession_useDefaultHashAlgorithmForSignableHash_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableHash = new SignableHash("Test data".getBytes()); - - toNotificationSignatureSessionRequestBuilder(b -> b - .withSignableData(null) - .withSignableHash(signableHash)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_overrideDefaultHashAlgorithmForSignableHash_ok(HashAlgorithm hashAlgorithm) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); - - toNotificationSignatureSessionRequestBuilder(b -> b - .withSignableData(null) - .withSignableHash(signableHash)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Test - void initSignatureSession_useDefaultHashAlgorithmForSignableData_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableData = new SignableData("Test data".getBytes()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_overrideDefaultHashAlgorithmForSignableData_ok(HashAlgorithm hashAlgorithm) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); - - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.capabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyUUID_throwException(String relyingPartyUUID) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyName_throwException(String relyingPartyName) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_semanticIdentifierAndDocumentNumberAreBothSet_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(DOCUMENT_NUMBER).withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", ex.getMessage()); - } - - @Test - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - assertEquals("Value for 'digestInput' has already been set with SignableData", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - assertEquals("Value for 'digestInput' has already been set with SignableHash", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_interactionsAreNotProvided_throwException(List interactions) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdClientException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) - void initSignatureSession_duplicateInteractionsProvided_throwException(List interactions) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initSignatureSession_invalidNonce(String nonce) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'nonce' length must be between 1 and 30 characters", ex.getMessage()); - } - } - - @Nested - class ResponseValidationTests { - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingSessionID_throwException(String sessionID) { - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(sessionID, new VerificationCode(null, null)); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullSource - void validateResponseParameters_missingVerificationCode_throwException(VerificationCode verificationCode) { - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingVerificationCodeType_throwException(String vcType) { - var verificationCode = new VerificationCode(vcType, null); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.type' is missing or empty", ex.getMessage()); - } - - @Test - void validateResponse_unsupportedVerificationCodeType_throwException() { - var verificationCode = new VerificationCode("unsupportedType", null); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.type' contains unsupported value", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingVerificationCodeValue_throwException(String vcValue) { - var verificationCode = new VerificationCode("numeric4", vcValue); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.value' is missing or empty", ex.getMessage()); - } - - @Test - void validateResponse_verificationCodeDoesNotMatchPattern_throwException() { - var verificationCode = new VerificationCode("numeric4", "aaaaaa"); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.value' does not match the required pattern", ex.getMessage()); - } - } - - private NotificationSignatureSessionRequestBuilder toNotificationSignatureSessionRequestBuilder(UnaryOperator modifier) { - return modifier.apply(toBaseNotificationSignatureSessionRequestBuilder()); - } - - private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSessionRequestBuilder() { - return new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); - } - - private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { - var verificationCode = new VerificationCode("numeric4", "4927"); - return toNotificationSignatureSessionResponse(verificationCode); - } - - private static NotificationSignatureSessionResponse toNotificationSignatureSessionResponse(VerificationCode verificationCode) { - return new NotificationSignatureSessionResponse("00000000-0000-0000-0000-000000000000", verificationCode); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse signature) { - assertNotNull(signature); - assertEquals("00000000-0000-0000-0000-000000000000", signature.sessionID()); - assertEquals("numeric4", signature.vc().type()); - assertEquals("4927", signature.vc().value()); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, null), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), - Arguments.of(CertificateLevel.QSCD, "QSCD") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.VerificationCode; + +class NotificationSignatureSessionRequestBuilderTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = toBaseNotificationSignatureSessionRequestBuilder().initSignatureSession(); + + assertSessionResponse(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(SEMANTICS_IDENTIFIER)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); + } + + @Test + void initSignatureSession_withDocumentNumber_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = toNotificationSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(null).withDocumentNumber(DOCUMENT_NUMBER)) + .initSignatureSession(); + + assertSessionResponse(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initSignatureSession_withRequestProperties_ok(boolean shareIp) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(shareIp)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.requestProperties()); + assertEquals(shareIp, capturedRequest.requestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Test + void initSignatureSession_useDefaultHashAlgorithmForSignableHash_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableHash = new SignableHash("Test data".getBytes()); + + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_overrideDefaultHashAlgorithmForSignableHash_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); + + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Test + void initSignatureSession_useDefaultHashAlgorithmForSignableData_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableData = new SignableData("Test data".getBytes()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_overrideDefaultHashAlgorithmForSignableData_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); + + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyUUID_throwException(String relyingPartyUUID) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyName_throwException(String relyingPartyName) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_semanticIdentifierAndDocumentNumberAreBothSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(DOCUMENT_NUMBER).withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", ex.getMessage()); + } + + @Test + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableData", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableHash", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_interactionsAreNotProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdClientException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initSignatureSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters", ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingSessionID_throwException(String sessionID) { + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(sessionID, new VerificationCode(null, null)); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullSource + void validateResponseParameters_missingVerificationCode_throwException(VerificationCode verificationCode) { + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeType_throwException(String vcType) { + var verificationCode = new VerificationCode(vcType, null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' is missing or empty", ex.getMessage()); + } + + @Test + void validateResponse_unsupportedVerificationCodeType_throwException() { + var verificationCode = new VerificationCode("unsupportedType", null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' contains unsupported value", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeValue_throwException(String vcValue) { + var verificationCode = new VerificationCode("numeric4", vcValue); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' is missing or empty", ex.getMessage()); + } + + @Test + void validateResponse_verificationCodeDoesNotMatchPattern_throwException() { + var verificationCode = new VerificationCode("numeric4", "aaaaaa"); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' does not match the required pattern", ex.getMessage()); + } + } + + private NotificationSignatureSessionRequestBuilder toNotificationSignatureSessionRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationSignatureSessionRequestBuilder()); + } + + private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSessionRequestBuilder() { + return new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); + } + + private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { + var verificationCode = new VerificationCode("numeric4", "4927"); + return toNotificationSignatureSessionResponse(verificationCode); + } + + private static NotificationSignatureSessionResponse toNotificationSignatureSessionResponse(VerificationCode verificationCode) { + return new NotificationSignatureSessionResponse("00000000-0000-0000-0000-000000000000", verificationCode); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse signature) { + assertNotNull(signature); + assertEquals("00000000-0000-0000-0000-000000000000", signature.sessionID()); + assertEquals("numeric4", signature.vc().type()); + assertEquals("4927", signature.vc().value()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index f0c4560c..203cca33 100644 --- a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -1,210 +1,210 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URI; -import java.util.Base64; -import java.util.regex.Pattern; - -import javax.imageio.ImageIO; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class QrCodeGeneratorTest { - - private static final int WHITE_COLOR = -1; - - @Test - void generateDataUri_validateQrContent() { - URI uri = createUri(); - String base64ImageData = QrCodeGenerator.generateDataUri(uri.toString()); - - assertNotNull(base64ImageData); - String[] parts = base64ImageData.split(","); - assertEquals("data:image/png;base64", parts[0]); - assertEquals(uri.toString(), QrCodeUtil.extractQrContent(parts[1]).getText()); - } - - @Nested - class DefaultValues { - - @Test - void generateDataUri() { - URI uri = createUri(); - String qrDataUri = QrCodeGenerator.generateDataUri(uri.toString()); - String imgBase64 = qrDataUri.split(",")[1]; - BufferedImage qrImage = convertToBufferedImage(imgBase64); - - assertEquals(610, qrImage.getHeight()); - assertEquals(610, qrImage.getHeight()); - assertTrue(validateQuietArea(qrImage, 4, 10)); - assertQrModuleSize(qrImage, 4, 10); - } - - @Test - void generateBufferedImage() { - URI uri = createUri(); - BufferedImage qrImage = QrCodeGenerator.generateImage(uri.toString()); - - assertEquals(610, qrImage.getHeight()); - assertEquals(610, qrImage.getHeight()); - assertTrue(validateQuietArea(qrImage, 4, 10)); - assertQrModuleSize(qrImage, 4, 10); - } - } - - @Test - void generateImage_providedCustomValues() { - URI uri = createUri(); - int quietAreaSize = 2; - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 100, 100, quietAreaSize); - - assertEquals(100, bufferedImage.getHeight()); - assertEquals(100, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - float expectedModuleSize = (float) bufferedImage.getWidth() / (53 + 2 * quietAreaSize); - assertQrModuleSize(bufferedImage, 2, expectedModuleSize); - } - - @Nested - class QrCodeModulePixelRanges { - - @Test - void generateImage_providedCustomValues_moduleSize6px() { - URI uri = createUri(); - int quietAreaSize = 2; - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 366, 366, quietAreaSize); - - assertEquals(366, bufferedImage.getHeight()); - assertEquals(366, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - assertQrModuleSize(bufferedImage, 4, 6); - } - - @Test - void generateImage_providedCustomValues_moduleSize19px() { - URI uri = createUri(); - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 1159, 1159, 4); - - assertEquals(1159, bufferedImage.getHeight()); - assertEquals(1159, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - assertQrModuleSize(bufferedImage, 4, 19); - } - } - - @ParameterizedTest - @NullAndEmptySource - void generateImage_providedDataIsEmpty_throwException(String data) { - var ex = assertThrows(SmartIdClientException.class, () -> QrCodeGenerator.generateImage(data, 10, 10, 2)); - - assertEquals("Provided data cannot be empty", ex.getMessage()); - } - - @Test - void convertToBase64() { - URI uri = createUri(); - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString()); - String base64ImageData = QrCodeGenerator.convertToDataUri(bufferedImage, "png"); - - String[] parts = base64ImageData.split(","); - assertEquals("data:image/png;base64", parts[0]); - Pattern pattern = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); - assertTrue(pattern.matcher(parts[1].replaceAll("\\s", "")).matches()); - } - - private static URI createUri() { - var linkBuilder = new DeviceLinkBuilder() - .withDeviceLinkBase("smartid://link") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") - .withElapsedSeconds(1L) - .withRelyingPartyName("DEMO") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions("interactions") - .withLang("ENG"); - - return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); - } - - private static BufferedImage convertToBufferedImage(String qrDataUri) { - byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); - try (ByteArrayInputStream bis = new ByteArrayInputStream(qrCodeBytes)) { - return ImageIO.read(bis); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static boolean validateQuietArea(BufferedImage qrImage, int quietZoneModules, int moduleSize) { - int quietZonePixelSize = quietZoneModules * moduleSize; - - // Validate top and bottom quiet areas - for (int y = 0; y < quietZonePixelSize; y++) { - for (int x = 0; x < qrImage.getWidth(); x++) { - if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(x, qrImage.getHeight() - 1 - y) != WHITE_COLOR) { - return false; - } - } - } - // Validate left and right quiet areas - for (int x = 0; x < quietZonePixelSize; x++) { - for (int y = 0; y < qrImage.getHeight(); y++) { - if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(qrImage.getWidth() - 1 - x, y) != WHITE_COLOR) { - return false; - } - } - } - return true; - } - - private static void assertQrModuleSize(BufferedImage qrImage, - int nrOfQuietAreaModules, - float expectedModuleSizePx) { - float qrCodeWidth = 53 * expectedModuleSizePx; - float quiteAreaWidth = nrOfQuietAreaModules * expectedModuleSizePx; - float expectedWidth = qrCodeWidth + 2 * quiteAreaWidth; - assertEquals(expectedWidth, qrImage.getWidth()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.Base64; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class QrCodeGeneratorTest { + + private static final int WHITE_COLOR = -1; + + @Test + void generateDataUri_validateQrContent() { + URI uri = createUri(); + String base64ImageData = QrCodeGenerator.generateDataUri(uri.toString()); + + assertNotNull(base64ImageData); + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + assertEquals(uri.toString(), QrCodeUtil.extractQrContent(parts[1]).getText()); + } + + @Nested + class DefaultValues { + + @Test + void generateDataUri() { + URI uri = createUri(); + String qrDataUri = QrCodeGenerator.generateDataUri(uri.toString()); + String imgBase64 = qrDataUri.split(",")[1]; + BufferedImage qrImage = convertToBufferedImage(imgBase64); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + + @Test + void generateBufferedImage() { + URI uri = createUri(); + BufferedImage qrImage = QrCodeGenerator.generateImage(uri.toString()); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + } + + @Test + void generateImage_providedCustomValues() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 100, 100, quietAreaSize); + + assertEquals(100, bufferedImage.getHeight()); + assertEquals(100, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + float expectedModuleSize = (float) bufferedImage.getWidth() / (53 + 2 * quietAreaSize); + assertQrModuleSize(bufferedImage, 2, expectedModuleSize); + } + + @Nested + class QrCodeModulePixelRanges { + + @Test + void generateImage_providedCustomValues_moduleSize6px() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 366, 366, quietAreaSize); + + assertEquals(366, bufferedImage.getHeight()); + assertEquals(366, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 6); + } + + @Test + void generateImage_providedCustomValues_moduleSize19px() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 1159, 1159, 4); + + assertEquals(1159, bufferedImage.getHeight()); + assertEquals(1159, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 19); + } + } + + @ParameterizedTest + @NullAndEmptySource + void generateImage_providedDataIsEmpty_throwException(String data) { + var ex = assertThrows(SmartIdClientException.class, () -> QrCodeGenerator.generateImage(data, 10, 10, 2)); + + assertEquals("Provided data cannot be empty", ex.getMessage()); + } + + @Test + void convertToBase64() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString()); + String base64ImageData = QrCodeGenerator.convertToDataUri(bufferedImage, "png"); + + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + Pattern pattern = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); + assertTrue(pattern.matcher(parts[1].replaceAll("\\s", "")).matches()); + } + + private static URI createUri() { + var linkBuilder = new DeviceLinkBuilder() + .withDeviceLinkBase("smartid://link") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") + .withElapsedSeconds(1L) + .withRelyingPartyName("DEMO") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions("interactions") + .withLang("ENG"); + + return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); + } + + private static BufferedImage convertToBufferedImage(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream bis = new ByteArrayInputStream(qrCodeBytes)) { + return ImageIO.read(bis); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static boolean validateQuietArea(BufferedImage qrImage, int quietZoneModules, int moduleSize) { + int quietZonePixelSize = quietZoneModules * moduleSize; + + // Validate top and bottom quiet areas + for (int y = 0; y < quietZonePixelSize; y++) { + for (int x = 0; x < qrImage.getWidth(); x++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(x, qrImage.getHeight() - 1 - y) != WHITE_COLOR) { + return false; + } + } + } + // Validate left and right quiet areas + for (int x = 0; x < quietZonePixelSize; x++) { + for (int y = 0; y < qrImage.getHeight(); y++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(qrImage.getWidth() - 1 - x, y) != WHITE_COLOR) { + return false; + } + } + } + return true; + } + + private static void assertQrModuleSize(BufferedImage qrImage, + int nrOfQuietAreaModules, + float expectedModuleSizePx) { + float qrCodeWidth = 53 * expectedModuleSizePx; + float quiteAreaWidth = nrOfQuietAreaModules * expectedModuleSizePx; + float expectedWidth = qrCodeWidth + 2 * quiteAreaWidth; + assertEquals(expectedWidth, qrImage.getWidth()); + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeUtil.java b/src/test/java/ee/sk/smartid/QrCodeUtil.java index 5039bd55..23ddb00c 100644 --- a/src/test/java/ee/sk/smartid/QrCodeUtil.java +++ b/src/test/java/ee/sk/smartid/QrCodeUtil.java @@ -1,69 +1,69 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.Base64; - -import javax.imageio.ImageIO; - -import com.google.zxing.BinaryBitmap; -import com.google.zxing.NotFoundException; -import com.google.zxing.Result; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.multi.qrcode.QRCodeMultiReader; - -public class QrCodeUtil { - - private QrCodeUtil() { - } - - public static Result extractQrContent(String qrDataUri) { - BinaryBitmap bitmap = getBinaryBitmap(qrDataUri); - Result result; - try { - result = Arrays.stream(new QRCodeMultiReader().decodeMultiple(bitmap)).findFirst().get(); - } catch (NotFoundException ex) { - throw new RuntimeException(ex); - } - return result; - } - - public static BinaryBitmap getBinaryBitmap(String qrDataUri) { - byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes)) { - BufferedImage bufferedImage = ImageIO.read(inputStream); - return new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; + +import javax.imageio.ImageIO; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.multi.qrcode.QRCodeMultiReader; + +public class QrCodeUtil { + + private QrCodeUtil() { + } + + public static Result extractQrContent(String qrDataUri) { + BinaryBitmap bitmap = getBinaryBitmap(qrDataUri); + Result result; + try { + result = Arrays.stream(new QRCodeMultiReader().decodeMultiple(bitmap)).findFirst().get(); + } catch (NotFoundException ex) { + throw new RuntimeException(ex); + } + return result; + } + + public static BinaryBitmap getBinaryBitmap(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes)) { + BufferedImage bufferedImage = ImageIO.read(inputStream); + return new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java index 2069e9b9..cf19d89a 100644 --- a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java @@ -1,155 +1,155 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class QualifiedSignatureCertificatePurposeValidatorTest { - - private static final String QUALIFIED_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); - private static final String SK_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; - private static final String QCP_N_QSCD_OID = "0.4.0.194112.1.2"; - - private QualifiedSignatureCertificatePurposeValidator validator; - - @BeforeEach - void setUp() { - validator = new QualifiedSignatureCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(QUALIFIED_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) - void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); - } - - @Test - void validate_QsStatementsExtensionIsMissing_throwException() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); - } - - @Test - void validate_qsStatementsDoesNotHaveElectronicSigning_throwException() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - - QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); - X509Certificate cert = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) - .withQcStatement(qcStatement) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); - } - - private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class QualifiedSignatureCertificatePurposeValidatorTest { + + private static final String QUALIFIED_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String SK_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; + private static final String QCP_N_QSCD_OID = "0.4.0.194112.1.2"; + + private QualifiedSignatureCertificatePurposeValidator validator; + + @BeforeEach + void setUp() { + validator = new QualifiedSignatureCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(QUALIFIED_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + @Test + void validate_QsStatementsExtensionIsMissing_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); + } + + @Test + void validate_qsStatementsDoesNotHaveElectronicSigning_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + + QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .withQcStatement(qcStatement) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java index 79ddd848..ef2c984a 100644 --- a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java @@ -1,69 +1,69 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class RpChallengeGeneratorTest { - - @Test - void generate_defaultValueUsed() { - RpChallenge challenge = RpChallengeGenerator.generate(); - - assertNotNull(challenge); - assertEquals(64, challenge.value().length); - } - - @ParameterizedTest - @ValueSource(ints = {32, 43, 59, 64}) - void generate_providedValuesAreInAllowedRange(int allowedValue) { - RpChallenge challenge = RpChallengeGenerator.generate(allowedValue); - - assertNotNull(challenge); - assertEquals(allowedValue, challenge.value().length); - } - - @Test - void generate_providedValueIsLessThanAllowed_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(31)); - assertEquals("Length must be between 32 and 64", ex.getMessage()); - } - - @Test - void generate_providedValueIsMoreThanAllowed_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(65)); - assertEquals("Length must be between 32 and 64", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class RpChallengeGeneratorTest { + + @Test + void generate_defaultValueUsed() { + RpChallenge challenge = RpChallengeGenerator.generate(); + + assertNotNull(challenge); + assertEquals(64, challenge.value().length); + } + + @ParameterizedTest + @ValueSource(ints = {32, 43, 59, 64}) + void generate_providedValuesAreInAllowedRange(int allowedValue) { + RpChallenge challenge = RpChallengeGenerator.generate(allowedValue); + + assertNotNull(challenge); + assertEquals(allowedValue, challenge.value().length); + } + + @Test + void generate_providedValueIsLessThanAllowed_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(31)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); + } + + @Test + void generate_providedValueIsMoreThanAllowed_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(65)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java index 4eb6df4e..9d367811 100644 --- a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -1,65 +1,65 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; -import ee.sk.smartid.exception.permanent.ProtocolFailureException; -import ee.sk.smartid.exception.permanent.SmartIdServerException; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; - -public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("USER_REFUSED", UserRefusedException.class), - Arguments.of("TIMEOUT", SessionTimeoutException.class), - Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), - Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), - Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), - Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), - Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), - Arguments.of("SERVER_ERROR", SmartIdServerException.class), - Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class), - Arguments.of("ACCOUNT_UNUSABLE", UserAccountUnusableException.class) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; + +public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), + Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), + Arguments.of("SERVER_ERROR", SmartIdServerException.class), + Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class), + Arguments.of("ACCOUNT_UNUSABLE", UserAccountUnusableException.class) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java index 82d57a2d..3b04a6d9 100644 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Base64; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class SignableDataTest { - - private static final byte[] TEST_DATA = "Test data".getBytes(); - - @Test - void getDigestInBase64() { - SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); - assertEquals(Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512)), signableData.getDigestInBase64()); - assertEquals(HashAlgorithm.SHA_512, signableData.hashAlgorithm()); - } - - @Test - void calculateHash() { - SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); - assertArrayEquals(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512), signableData.calculateHash()); - } - - @ParameterizedTest - @NullAndEmptySource - void emptyHashProvided_throwException(byte[] dataToSign) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(dataToSign)); - assertEquals("Parameter 'dataToSign' cannot be empty", ex.getMessage()); - } - - @Test - void defaultHashAlgorithmSetToNull_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(TEST_DATA, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableDataTest { + + private static final byte[] TEST_DATA = "Test data".getBytes(); + + @Test + void getDigestInBase64() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertEquals(Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512)), signableData.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableData.hashAlgorithm()); + } + + @Test + void calculateHash() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertArrayEquals(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512), signableData.calculateHash()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashProvided_throwException(byte[] dataToSign) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(dataToSign)); + assertEquals("Parameter 'dataToSign' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmSetToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(TEST_DATA, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/SignableHashTest.java index 7d6bdf7c..e269662b 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/SignableHashTest.java @@ -1,64 +1,64 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Base64; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class SignableHashTest { - - private static final byte[] DIGEST = DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512); - - @Test - void getDigestInBase64() { - SignableHash signableHash = new SignableHash(DIGEST, HashAlgorithm.SHA_512); - - assertEquals(Base64.getEncoder().encodeToString(DIGEST), signableHash.getDigestInBase64()); - assertEquals(HashAlgorithm.SHA_512, signableHash.hashAlgorithm()); - } - - @ParameterizedTest - @NullAndEmptySource - void emptyHashValueProvided_throwException(byte[] hash) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(hash)); - assertEquals("Parameter 'hash' cannot be empty", ex.getMessage()); - } - - @Test - void defaultHashAlgorithmOverriddenToNull_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(DIGEST, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableHashTest { + + private static final byte[] DIGEST = DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512); + + @Test + void getDigestInBase64() { + SignableHash signableHash = new SignableHash(DIGEST, HashAlgorithm.SHA_512); + + assertEquals(Base64.getEncoder().encodeToString(DIGEST), signableHash.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableHash.hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashValueProvided_throwException(byte[] hash) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(hash)); + assertEquals("Parameter 'hash' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmOverriddenToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(DIGEST, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index 5f7d52d2..bf077014 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -1,587 +1,587 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; - -class SignatureResponseValidatorTest { - - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - - // TODO - 31.08.25: replace these values when the test accounts are available - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; - - private SignatureResponseValidator signatureResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - } - - @Test - void validate_validRawDigestSignature() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); - assertEquals("OK", response.getEndResult()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel); - assertEquals("OK", response.getEndResult()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @ParameterizedTest - @EnumSource(FlowType.class) - void validate_flowTypesAreSupported(FlowType flowType) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss", flowType); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); - assertEquals("OK", response.getEndResult()); - } - - @Test - void validate_nqSigning_ok() { - SessionStatus sessionStatus = toNqignatureSessionStatus(); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - assertEquals("OK", response.getEndResult()); - } - - @Test - void validate_stateParameterMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setState(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'state' is empty", ex.getMessage()); - } - - @Test - void validate_sessionNotComplete() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setState("RUNNING"); - - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertTrue(ex.getMessage().contains("Session is not complete")); - } - - @Test - void validate_sessionResultNull() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result' is missing", ex.getMessage()); - } - - @Test - void validate_missingDocumentNumber() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getResult().setDocumentNumber(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); - } - - @Test - void validate_missingInteractionFlowUsed() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setInteractionTypeUsed(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); - } - - @Test - void validate_signatureProtocolMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); - } - - @Nested - class CertificateValidation { - - @Test - void validate_missingCertificate() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setCert(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert' is missing", ex.getMessage()); - } - - @Test - void validate_missingCertificateValue() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setValue(null); - - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); - } - - @Test - void validate_certificateLevelMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setCertificateLevel(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); - } - - @Test - void validate_certificateLevelMismatch() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setCertificateLevel("ADVANCED"); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - } - - @Nested - class SignatureValidation { - - @Test - void validate_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_unknownSignatureProtocol() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); - sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void validate_handleSessionEndResultErrors(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - } - - @Test - void validate_endResultMissing_throwsException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - sessionStatus.getResult().setEndResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); - } - - @Test - void validate_sessionStatusNull() { - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, CertificateLevel.QUALIFIED)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_signatureMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignature(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); - } - - @Test - void validate_signatureValueMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setValue(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); - } - - @Test - void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setValue("invalid-not+encoded+value"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithm(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "invalid"}) - void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_flowTypeMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setFlowType(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); - } - - @Test - void validate_invalidFlowType() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmNotSupported() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmNotRsassaPss() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); - sessionStatus.getSignature().setSignatureAlgorithm("rsa"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Nested - class SignatureAlgorithmParametersValidations { - - @Test - void validate_signatureAlgorithmParametersMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithmParameters(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_hashAlgorithmMissing(String hashAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); - } - - @Test - void validate_invalidHashAlgorithm() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_maskGenAlgorithmIsMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); - } - - @Test - void validate_invalidMaskGenAlgorithmName() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .setParameters(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm(hashAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); - } - - @Test - void validate_maskGenHashAlgorithmInvalid() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm("INVALID-HASH"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_mismatchedHashAlgorithms() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); - } - - @Test - void validate_saltLengthIsMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); - } - - @Test - void validate_invalidSaltLength() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); - } - - @Test - void validate_invalidTrailerField() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); - } - } - } - - - private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, - String signatureAlgorithm) { - return toQualifiedSignatureSessionStatus(signatureProtocol, signatureAlgorithm, FlowType.QR); - } - - private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, - String signatureAlgorithm, - FlowType flowType) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); - - var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params, flowType); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(signatureProtocol); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - return sessionStatus; - } - - private static SessionStatus toNqignatureSessionStatus() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("ADVANCED"); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); - - var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params, FlowType.QR); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(SignatureProtocol.RAW_DIGEST_SIGNATURE.name()); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - return sessionStatus; - } - - private static SessionSignature toSessionSignature(String signatureValue, - String signatureAlgorithm, - SessionSignatureAlgorithmParameters params, - FlowType flowType) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(params); - sessionSignature.setServerRandom("serverRandomValue"); - sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); - sessionSignature.setFlowType(flowType.getDescription()); - return sessionSignature; - } - - private static SessionSignatureAlgorithmParameters toSessionSignatureAlgorithmParams() { - var mgfParams = new SessionMaskGenAlgorithmParameters(); - mgfParams.setHashAlgorithm("SHA-512"); - - var mgf = new SessionMaskGenAlgorithm(); - mgf.setAlgorithm("id-mgf1"); - mgf.setParameters(mgfParams); - - var params = new SessionSignatureAlgorithmParameters(); - params.setHashAlgorithm("SHA-512"); - params.setMaskGenAlgorithm(mgf); - params.setSaltLength(64); - params.setTrailerField("0xbc"); - return params; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class SignatureResponseValidatorTest { + + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + + // TODO - 31.08.25: replace these values when the test accounts are available + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; + + private SignatureResponseValidator signatureResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + } + + @Test + void validate_validRawDigestSignature() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel); + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource(FlowType.class) + void validate_flowTypesAreSupported(FlowType flowType) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss", flowType); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + + @Test + void validate_nqSigning_ok() { + SessionStatus sessionStatus = toNqignatureSessionStatus(); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + assertEquals("OK", response.getEndResult()); + } + + @Test + void validate_stateParameterMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'state' is empty", ex.getMessage()); + } + + @Test + void validate_sessionNotComplete() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState("RUNNING"); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertTrue(ex.getMessage().contains("Session is not complete")); + } + + @Test + void validate_sessionResultNull() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result' is missing", ex.getMessage()); + } + + @Test + void validate_missingDocumentNumber() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getResult().setDocumentNumber(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); + } + + @Test + void validate_missingInteractionFlowUsed() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setInteractionTypeUsed(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); + } + + @Test + void validate_signatureProtocolMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); + } + + @Nested + class CertificateValidation { + + @Test + void validate_missingCertificate() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setCert(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert' is missing", ex.getMessage()); + } + + @Test + void validate_missingCertificateValue() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setValue(null); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); + } + + @Test + void validate_certificateLevelMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); + } + + @Test + void validate_certificateLevelMismatch() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel("ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + } + + @Nested + class SignatureValidation { + + @Test + void validate_rawDigestUnexpectedAlgorithm() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_unknownSignatureProtocol() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void validate_handleSessionEndResultErrors(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + } + + @Test + void validate_endResultMissing_throwsException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + sessionStatus.getResult().setEndResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); + } + + @Test + void validate_sessionStatusNull() { + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, CertificateLevel.QUALIFIED)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_signatureMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignature(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); + } + + @Test + void validate_signatureValueMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); + } + + @Test + void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue("invalid-not+encoded+value"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_flowTypeMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); + } + + @Test + void validate_invalidFlowType() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmNotSupported() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmNotRsassaPss() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); + sessionStatus.getSignature().setSignatureAlgorithm("rsa"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Nested + class SignatureAlgorithmParametersValidations { + + @Test + void validate_signatureAlgorithmParametersMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithmParameters(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmMissing(String hashAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void validate_invalidHashAlgorithm() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_maskGenAlgorithmIsMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); + } + + @Test + void validate_invalidMaskGenAlgorithmName() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .setParameters(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm(hashAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void validate_maskGenHashAlgorithmInvalid() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_mismatchedHashAlgorithms() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); + } + + @Test + void validate_saltLengthIsMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); + } + + @Test + void validate_invalidSaltLength() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); + } + + @Test + void validate_invalidTrailerField() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); + } + } + } + + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm) { + return toQualifiedSignatureSessionStatus(signatureProtocol, signatureAlgorithm, FlowType.QR); + } + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm, + FlowType flowType) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); + + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params, flowType); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(signatureProtocol); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static SessionStatus toNqignatureSessionStatus() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("ADVANCED"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); + + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params, FlowType.QR); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(SignatureProtocol.RAW_DIGEST_SIGNATURE.name()); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static SessionSignature toSessionSignature(String signatureValue, + String signatureAlgorithm, + SessionSignatureAlgorithmParameters params, + FlowType flowType) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(params); + sessionSignature.setServerRandom("serverRandomValue"); + sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); + sessionSignature.setFlowType(flowType.getDescription()); + return sessionSignature; + } + + private static SessionSignatureAlgorithmParameters toSessionSignatureAlgorithmParams() { + var mgfParams = new SessionMaskGenAlgorithmParameters(); + mgfParams.setHashAlgorithm("SHA-512"); + + var mgf = new SessionMaskGenAlgorithm(); + mgf.setAlgorithm("id-mgf1"); + mgf.setParameters(mgfParams); + + var params = new SessionSignatureAlgorithmParameters(); + params.setHashAlgorithm("SHA-512"); + params.setMaskGenAlgorithm(mgf); + params.setSaltLength(64); + params.setTrailerField("0xbc"); + return params; + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java index 4ab8d7ac..5dfff34e 100644 --- a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java @@ -1,121 +1,121 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Base64; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class SignatureValueValidatorImplTest { - - // TODO - 22.08.25: replace these values when test accounts are available - private static final String CERT = "MIIHSjCCBtCgAwIBAgIQBQHi3vqqZg+tDaGzQeB2GzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUwNzI5MDgxMTAzWhcNMjgwNzI4MDgxMTAyWjBfMQswCQYDVQQGEwJFRTEUMBIGA1UEAwwLTVVTRVIsVVJNQVMxDjAMBgNVBAQMBU1VU0VSMQ4wDAYDVQQqDAVVUk1BUzEaMBgGA1UEBRMRUE5PRUUtMzkwMDMwMTI3OTgwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCf8qQkO51SM/Gdw63LObpk4kwutMSqW345PU4HC+HqQ2H03fTludjY7iBCgEWmXQjoTt6vQgDGPfBlydjZiu2GUSCL/f2DTv76BuWzR/Jw6q4+R86GRhlMJFqfqE2gqCIddVbUx+qYZ37qCddqgIoRYejdrUeWopp2xzya5gt41FM9By95e3pS/1tug7aAlPoT3Tg18+13qqru1SDGxYW+0NVojesYX3Pzz8Exz2dWcFWwMqoU3SMlAULHDC9OPMtuZBSZA2tvyuD+CHHsU13LI46iDRU2j9BVr9EBuO/uvL3U5eIkX0gpy5bdo/TWmXDijTb5udXO9cz+GMaCQTx4yuBTnC31pHw/qrEp00FRZy7yiG0expv7w4c0YiziMFK8GfhnPmNAVEyjTWImmckK9SiIZH0F/oU1VZvtX3aXsmoTzEwpzAy3KPiKxJ0ZSSsVHV+G1nZvx/1mRxKcT+rOzNcx7iY9uAzin9ajPLYTukWsGVOTgQ2GxpYrEhuf8PvQlZ62BVIvfS5swhlwXzMU8oEAsHCpUVDNCLtckkKBgoy9pYZyKbXUtUP1TTEL3ZC9/4h3Udmao6JNWp5niyHDWVpF6r56O/ORZGx1GlT1P+G9rK6bBteptvNWillGPMA5E1fdwSci7/eH8amSED0CAy0rlq+0CdMdnpasqyG5oDmYJncWhhEozQ2fI7SkvNgSiMxDnJXhi8/Zvh4j+29eC7fqG5ZsLxQ1YqaK8XsIsNJ2Lxj0BhrEgU7Zz5lILUdOILEfU1S2Wi4Ow1P23dAP/O+o6u4SDSKSM2+C5s9daq/5zJ2w2s/B8JB8Mat5MPJuzKrvSnYMIUzQjtlsuMBRIRbHmHtCjDXufF11BOCLfPUYU5GDvk6MY51+p/hZrAowQHWZYI+271UxJR9I1dCTNvo1HsiNEnLSgdOikWvmykqiDVWPe6SiRpVKBQ7MkhgvF/CrHGG0S4GBuG6E2OHEMKl73CWuqU8MrPSOQvaXY7f99ZGK9RL1OG8oxRJpJNECAwEAAaOCAo8wggKLMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTM5MDAzMDEyNzk4LUZGTDgtUTB5BgNVHSAEcjBwMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCQYHBACL7EABAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5OTAwMzAxMTIwMDAwWjCBrgYIKwYBBQUHAQMEgaEwgZ4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwCAYGBACORgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJlbjA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUq5xLZIjeh1p1kreds8ie7OgpfmwwDgYDVR0PAQH/BAQDAgZAMAoGCCqGSM49BAMDA2gAMGUCMQCdrnNqlxbO/N6FELvGd4MHeNjTIpdDSj+6Htu6W7KRFleQGe8zhK9yA2l/zSerZvwCMGgbT0nvtgyoXBhSsUhY3RWTMiee4nKn7aBKqcmrDuHC9I9o67WpttfSE4srvL+qWQ=="; - private static final byte[] PAYLOAD = Base64.getDecoder().decode(("PGRzOlNpZ25lZEluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q" + "+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGE1MTIiPjwvZHM6U2lnbmF0dXJlTWV0aG9kPjxkczpSZWZlcmVuY2UgSWQ9InItaWQtNzcwMDA4OTNlNWU1YmVjOGMwY2IyOThjNmFkMGY0YTQtMSIgVVJJPSJkdW1teS5wZGYiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5QZmVkTkt1OHFaTUk1NXk1UkdIQmlUV0NZRTFvTXBwQi9VdnNHSVhtcmJRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PGRzOlJlZmVyZW5jZSBUeXBlPSJodHRwOi8vdXJpLmV0c2kub3JnLzAxOTAzI1NpZ25lZFByb3BlcnRpZXMiIFVSST0iI3hhZGVzLWlkLTc3MDAwODkzZTVlNWJlYzhjMGNiMjk4YzZhZDBmNGE0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPjFZd014blRUYmwwZXB5S0g0OEZ0WXFDb3pNbzAxem03NWpwV1pWNDJJNlk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+").getBytes(StandardCharsets.UTF_8)); - private static final byte[] SIGNATURE_VALUE = Base64.getDecoder().decode("UEVKOrz3Mr+qAXyOjGEt3Nnb8andzicBcEdbb4T2qVyGUslHdeJfgkXccPqBnjmEbL7xoU7eHVkO02K+XNseVY5UBHnXDlMBj1TyCnfelfiZFpAppgWmHKBXC11yE1OhtQ5/siaokPGBX1nZM2rzGlHYWxXYZrOGHCrm7gQEbClL342N6bEzeVVVPnxnxDEkzpNMFvY8UIL3C55WPPNKLBzFwSfduNcLaBiHc4ghaIiJebQc1h+Kad5OAYeu35v70k4HVePbDDp1Cb7RXfMRyUx7GNFSTZiKrG16XO8krp+d9T10SGRbZNoTzxvXBjtb8SjXM6Zvx0tiNdVnsWBrEylGzjS2DVU2+MDbek9QxlxqUU5E5H/WrelywgGTEzfZekowjofQjkYXaEAvNTK8x8Me1wIJThZwfrOy6H8MyPAdgAwl7fDwsZG6QhRCeG+9VY4CtmcII6YMZccCFCy9X3SJvXga4OcSrPi+Nwh3tfvJ5pkYvLliVKSCDpslTZk7JQYcQsJ1DVefMW6BfA+V3iX35mG/VHPo789BpzlZL6Ebs/dxNSEnyyWTDECFl2k2/B38w9jO4OuFLLg/U0AvM6ZLNlLWUjsKKg1s4U+SGlLc7r3hxaWCCwx4/NP2h8f3MTquxOCt+7WrjvCNOQ33bKcFGjYyCWpfGAfVgfMenp4oa40A1+Or+Px4Sd5yD3ZTnPSMYh2UzFZOiejGAS/koBYhn60P5PKRpEkC0nq+WQJD58soelH1EKifLoBtYNzhNOAuOfGRI5nEsW94TZ8hbC/mIEBmMnhKr9Lq/+glxbqskwOavWIF5xeWTKeSt2ErvgtNxX3hTlGxdNavwPi+/qtLikrNoirE26t1WFyPMaeH6hm0rIW42h5c0IvsXrQ4258uJzpZPe/RLbjdy62wi1S35PmowFUFImlHDKSIj4plEVXkrApZDV+/bL0VR6PNr7bsIZqgamL9OyLm6vTunP+A7Q+zpaZxuun17SC1QsthiGGBk03uf4CpNVVUpsO3".getBytes(StandardCharsets.UTF_8)); - - private SignatureValueValidator signatureValueValidator; - - @BeforeEach - void setUp() { - signatureValueValidator = new SignatureValueValidatorImpl(); - } - - @Test - void validate() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(CERT); - RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); - - assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); - } - - @ParameterizedTest - @ArgumentsSource(EmptyInputArgumentProvider.class) - void validate_InputParametersNotProvided_throwException(byte[] signatureValue, byte[] payload, X509Certificate certificate, RsaSsaPssParameters rsaSsaPssParameters) { - assertThrows(SmartIdClientException.class, () -> signatureValueValidator.validate(signatureValue, payload, certificate, rsaSsaPssParameters)); - } - - @Test - void validateSignatureValue_IsInvalid_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> signatureValueValidator.validate( - "invalidValue".getBytes(StandardCharsets.UTF_8), - PAYLOAD, - CertificateUtil.toX509CertificateFromEncodedString(CERT), - toRsaSsaPssParameters())); - assertEquals("Signature value validation failed", ex.getMessage()); - } - - @Test - void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> signatureValueValidator.validate( - SIGNATURE_VALUE, - "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), - CertificateUtil.toX509CertificateFromEncodedString(CERT), - toRsaSsaPssParameters())); - assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); - } - - private static RsaSsaPssParameters toRsaSsaPssParameters() { - RsaSsaPssParameters rsaSsaPssParameters = new RsaSsaPssParameters(); - rsaSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.SHA_512); - rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); - rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); - rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); - rsaSsaPssParameters.setTrailerField(TrailerField.BC); - return rsaSsaPssParameters; - } - - private static class EmptyInputArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) throws CertificateException { - return Stream.of( - Arguments.of(null, null, null, null), - Arguments.of(new byte[0], null, null, null), - Arguments.of(new byte[0], new byte[0], null, null), - Arguments.of(new byte[0], new byte[0], CertificateUtil.toX509CertificateFromEncodedString(CERT), null) - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class SignatureValueValidatorImplTest { + + // TODO - 22.08.25: replace these values when test accounts are available + private static final String CERT = "MIIHSjCCBtCgAwIBAgIQBQHi3vqqZg+tDaGzQeB2GzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUwNzI5MDgxMTAzWhcNMjgwNzI4MDgxMTAyWjBfMQswCQYDVQQGEwJFRTEUMBIGA1UEAwwLTVVTRVIsVVJNQVMxDjAMBgNVBAQMBU1VU0VSMQ4wDAYDVQQqDAVVUk1BUzEaMBgGA1UEBRMRUE5PRUUtMzkwMDMwMTI3OTgwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCf8qQkO51SM/Gdw63LObpk4kwutMSqW345PU4HC+HqQ2H03fTludjY7iBCgEWmXQjoTt6vQgDGPfBlydjZiu2GUSCL/f2DTv76BuWzR/Jw6q4+R86GRhlMJFqfqE2gqCIddVbUx+qYZ37qCddqgIoRYejdrUeWopp2xzya5gt41FM9By95e3pS/1tug7aAlPoT3Tg18+13qqru1SDGxYW+0NVojesYX3Pzz8Exz2dWcFWwMqoU3SMlAULHDC9OPMtuZBSZA2tvyuD+CHHsU13LI46iDRU2j9BVr9EBuO/uvL3U5eIkX0gpy5bdo/TWmXDijTb5udXO9cz+GMaCQTx4yuBTnC31pHw/qrEp00FRZy7yiG0expv7w4c0YiziMFK8GfhnPmNAVEyjTWImmckK9SiIZH0F/oU1VZvtX3aXsmoTzEwpzAy3KPiKxJ0ZSSsVHV+G1nZvx/1mRxKcT+rOzNcx7iY9uAzin9ajPLYTukWsGVOTgQ2GxpYrEhuf8PvQlZ62BVIvfS5swhlwXzMU8oEAsHCpUVDNCLtckkKBgoy9pYZyKbXUtUP1TTEL3ZC9/4h3Udmao6JNWp5niyHDWVpF6r56O/ORZGx1GlT1P+G9rK6bBteptvNWillGPMA5E1fdwSci7/eH8amSED0CAy0rlq+0CdMdnpasqyG5oDmYJncWhhEozQ2fI7SkvNgSiMxDnJXhi8/Zvh4j+29eC7fqG5ZsLxQ1YqaK8XsIsNJ2Lxj0BhrEgU7Zz5lILUdOILEfU1S2Wi4Ow1P23dAP/O+o6u4SDSKSM2+C5s9daq/5zJ2w2s/B8JB8Mat5MPJuzKrvSnYMIUzQjtlsuMBRIRbHmHtCjDXufF11BOCLfPUYU5GDvk6MY51+p/hZrAowQHWZYI+271UxJR9I1dCTNvo1HsiNEnLSgdOikWvmykqiDVWPe6SiRpVKBQ7MkhgvF/CrHGG0S4GBuG6E2OHEMKl73CWuqU8MrPSOQvaXY7f99ZGK9RL1OG8oxRJpJNECAwEAAaOCAo8wggKLMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTM5MDAzMDEyNzk4LUZGTDgtUTB5BgNVHSAEcjBwMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCQYHBACL7EABAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5OTAwMzAxMTIwMDAwWjCBrgYIKwYBBQUHAQMEgaEwgZ4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwCAYGBACORgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJlbjA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUq5xLZIjeh1p1kreds8ie7OgpfmwwDgYDVR0PAQH/BAQDAgZAMAoGCCqGSM49BAMDA2gAMGUCMQCdrnNqlxbO/N6FELvGd4MHeNjTIpdDSj+6Htu6W7KRFleQGe8zhK9yA2l/zSerZvwCMGgbT0nvtgyoXBhSsUhY3RWTMiee4nKn7aBKqcmrDuHC9I9o67WpttfSE4srvL+qWQ=="; + private static final byte[] PAYLOAD = Base64.getDecoder().decode(("PGRzOlNpZ25lZEluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q" + "+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGE1MTIiPjwvZHM6U2lnbmF0dXJlTWV0aG9kPjxkczpSZWZlcmVuY2UgSWQ9InItaWQtNzcwMDA4OTNlNWU1YmVjOGMwY2IyOThjNmFkMGY0YTQtMSIgVVJJPSJkdW1teS5wZGYiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5QZmVkTkt1OHFaTUk1NXk1UkdIQmlUV0NZRTFvTXBwQi9VdnNHSVhtcmJRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PGRzOlJlZmVyZW5jZSBUeXBlPSJodHRwOi8vdXJpLmV0c2kub3JnLzAxOTAzI1NpZ25lZFByb3BlcnRpZXMiIFVSST0iI3hhZGVzLWlkLTc3MDAwODkzZTVlNWJlYzhjMGNiMjk4YzZhZDBmNGE0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPjFZd014blRUYmwwZXB5S0g0OEZ0WXFDb3pNbzAxem03NWpwV1pWNDJJNlk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+").getBytes(StandardCharsets.UTF_8)); + private static final byte[] SIGNATURE_VALUE = Base64.getDecoder().decode("UEVKOrz3Mr+qAXyOjGEt3Nnb8andzicBcEdbb4T2qVyGUslHdeJfgkXccPqBnjmEbL7xoU7eHVkO02K+XNseVY5UBHnXDlMBj1TyCnfelfiZFpAppgWmHKBXC11yE1OhtQ5/siaokPGBX1nZM2rzGlHYWxXYZrOGHCrm7gQEbClL342N6bEzeVVVPnxnxDEkzpNMFvY8UIL3C55WPPNKLBzFwSfduNcLaBiHc4ghaIiJebQc1h+Kad5OAYeu35v70k4HVePbDDp1Cb7RXfMRyUx7GNFSTZiKrG16XO8krp+d9T10SGRbZNoTzxvXBjtb8SjXM6Zvx0tiNdVnsWBrEylGzjS2DVU2+MDbek9QxlxqUU5E5H/WrelywgGTEzfZekowjofQjkYXaEAvNTK8x8Me1wIJThZwfrOy6H8MyPAdgAwl7fDwsZG6QhRCeG+9VY4CtmcII6YMZccCFCy9X3SJvXga4OcSrPi+Nwh3tfvJ5pkYvLliVKSCDpslTZk7JQYcQsJ1DVefMW6BfA+V3iX35mG/VHPo789BpzlZL6Ebs/dxNSEnyyWTDECFl2k2/B38w9jO4OuFLLg/U0AvM6ZLNlLWUjsKKg1s4U+SGlLc7r3hxaWCCwx4/NP2h8f3MTquxOCt+7WrjvCNOQ33bKcFGjYyCWpfGAfVgfMenp4oa40A1+Or+Px4Sd5yD3ZTnPSMYh2UzFZOiejGAS/koBYhn60P5PKRpEkC0nq+WQJD58soelH1EKifLoBtYNzhNOAuOfGRI5nEsW94TZ8hbC/mIEBmMnhKr9Lq/+glxbqskwOavWIF5xeWTKeSt2ErvgtNxX3hTlGxdNavwPi+/qtLikrNoirE26t1WFyPMaeH6hm0rIW42h5c0IvsXrQ4258uJzpZPe/RLbjdy62wi1S35PmowFUFImlHDKSIj4plEVXkrApZDV+/bL0VR6PNr7bsIZqgamL9OyLm6vTunP+A7Q+zpaZxuun17SC1QsthiGGBk03uf4CpNVVUpsO3".getBytes(StandardCharsets.UTF_8)); + + private SignatureValueValidator signatureValueValidator; + + @BeforeEach + void setUp() { + signatureValueValidator = new SignatureValueValidatorImpl(); + } + + @Test + void validate() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(CERT); + RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); + + assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); + } + + @ParameterizedTest + @ArgumentsSource(EmptyInputArgumentProvider.class) + void validate_InputParametersNotProvided_throwException(byte[] signatureValue, byte[] payload, X509Certificate certificate, RsaSsaPssParameters rsaSsaPssParameters) { + assertThrows(SmartIdClientException.class, () -> signatureValueValidator.validate(signatureValue, payload, certificate, rsaSsaPssParameters)); + } + + @Test + void validateSignatureValue_IsInvalid_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + "invalidValue".getBytes(StandardCharsets.UTF_8), + PAYLOAD, + CertificateUtil.toX509CertificateFromEncodedString(CERT), + toRsaSsaPssParameters())); + assertEquals("Signature value validation failed", ex.getMessage()); + } + + @Test + void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + SIGNATURE_VALUE, + "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), + CertificateUtil.toX509CertificateFromEncodedString(CERT), + toRsaSsaPssParameters())); + assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); + } + + private static RsaSsaPssParameters toRsaSsaPssParameters() { + RsaSsaPssParameters rsaSsaPssParameters = new RsaSsaPssParameters(); + rsaSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); + rsaSsaPssParameters.setTrailerField(TrailerField.BC); + return rsaSsaPssParameters; + } + + private static class EmptyInputArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) throws CertificateException { + return Stream.of( + Arguments.of(null, null, null, null), + Arguments.of(new byte[0], null, null, null), + Arguments.of(new byte[0], new byte[0], null, null), + Arguments.of(new byte[0], new byte[0], CertificateUtil.toX509CertificateFromEncodedString(CERT), null) + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index a44def42..04b5c8de 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -1,811 +1,811 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.time.Duration; -import java.time.Instant; -import java.util.List; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.VerificationCode; - -class SmartIdClientTest { - - private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - private static final String PERSON_CODE = "PNOEE-1234567890"; - private static final String INITIAL_CALLBACK_URL = "https://example.com/callback"; - - private SmartIdClient smartIdClient; - - @BeforeEach - void setUp() { - smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); - smartIdClient.setRelyingPartyName("DEMO"); - smartIdClient.setHostUrl("http://localhost:18089"); - smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkCertificateChoiceSession { - - @Test - void createSameDeviceCertificateChoiceSession() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createSameDeviceCertificateChoiceSessionWithAllFields() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .withNonce("d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk") - .withShareMdClientIpAddress(true) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createQrCodeCertificateChoiceSession() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationCertificateChoiceSession { - - @Test - void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - - NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - } - - @Test - void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - - NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withShareMdClientIpAddress(true) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkAuthenticationSession { - - @Test - void createDeviceLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withDocumentNumber(DOCUMENT_NUMBER) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkSignatureSession { - - @Test - void createDeviceLinkSignature_withDocumentNumberSameDevice() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withDocumentNumberQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class CertificateByDocumentNumberRequest { - - @Test - void createCertificateRequest_withDocumentNumber() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", - "requests/sign/certificate-by-document-number-request-all-fields.json", - "responses/certificate-by-document-number-response.json"); - - CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.ADVANCED) - .getCertificateByDocumentNumber(); - - assertNotNull(response); - assertEquals(CertificateLevel.QUALIFIED, response.certificateLevel()); - assertNotNull(response.certificate()); - } - - @Test - void getCertificateByDocumentNumber_withUnknownState_throwsException() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", - "requests/sign/certificate-by-document-number-request-all-fields.json", - "responses/certificate-by-document-number-response-unknown-state.json"); - - CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.ADVANCED); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationAuthenticationSession { - - @Test - void createNotificationAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - } - - @Test - void createNotificationAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withDocumentNumber(DOCUMENT_NUMBER) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationBasedSignatureSession { - - @Test - void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(64).getBytes()); - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertSessionResponse(response); - } - - @Test - void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(64).getBytes()); - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertSessionResponse(response); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse response) { - assertNotNull(response.sessionID()); - VerificationCode verificationCode = response.vc(); - assertNotNull(verificationCode); - assertNotNull(verificationCode.type()); - assertNotNull(verificationCode.value()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class LinkedNotificationBasedSignatureSession { - - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - - @Test - void createLinkedNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, - "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", - "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withLinkedSessionID("10000000-0000-000-000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) - .initSignatureSession(); - - assertNotNull(response); - } - - @Test - void createLinkedNotificationSignature_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, - "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", - "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withLinkedSessionID("10000000-0000-000-000-000000000000") - .withNonce("cmFuZG9tTm9uY2U=") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) - .withShareMdClientIpAddress(true) - .initSignatureSession(); - - assertNotNull(response); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class SessionsStatus { - - @Test - void fetchFinalSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-successful-authentication.json"); - - SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); - - assertEquals("COMPLETE", status.getState()); - assertEquals("OK", status.getResult().getEndResult()); - } - - @Test - void getSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-running.json"); - - SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); - - assertEquals("RUNNING", status.getState()); - assertNull(status.getResult()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForAuth { - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInitialCallbackUrl(request.initialCallbackUrl()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.AUTHENTICATION, deviceLinkType, response.sessionToken()); - } - - @Test - void createDynamicContent_authenticationWithQRCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_authenticationWithQRCodeImage() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForSignature { - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.SIGNATURE, deviceLinkType, response.sessionToken()); - } - - @Test - void createDynamicContent_withQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withElapsedSeconds(elapsed.getSeconds()) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_withQrCodeImage() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withElapsedSeconds(elapsed.getSeconds()) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForCertificateChoice { - - @Test - void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_createQrCodeImage() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initCertificateChoice(); - - URI deviceLinkUri = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withInitialCallbackUrl("https://smart-id.com/callback") - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLinkUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.sessionToken()); - } - } - - private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { - assertEquals("https", qrCodeUri.getScheme()); - assertEquals("smart-id.com", qrCodeUri.getHost()); - assertEquals("/device-link/", qrCodeUri.getPath()); - - assertTrue(qrCodeUri.getQuery().contains("version=1.0")); - assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); - assertTrue(qrCodeUri.getQuery().contains("lang=eng")); - assertTrue(qrCodeUri.getQuery().contains("authCode=")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.VerificationCode; + +class SmartIdClientTest { + + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String PERSON_CODE = "PNOEE-1234567890"; + private static final String INITIAL_CALLBACK_URL = "https://example.com/callback"; + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("http://localhost:18089"); + smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkCertificateChoiceSession { + + @Test + void createSameDeviceCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createSameDeviceCertificateChoiceSessionWithAllFields() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .withNonce("d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk") + .withShareMdClientIpAddress(true) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createQrCodeCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationCertificateChoiceSession { + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + } + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withShareMdClientIpAddress(true) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkAuthenticationSession { + + @Test + void createDeviceLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withDocumentNumber(DOCUMENT_NUMBER) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkSignatureSession { + + @Test + void createDeviceLinkSignature_withDocumentNumberSameDevice() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withDocumentNumberQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class CertificateByDocumentNumberRequest { + + @Test + void createCertificateRequest_withDocumentNumber() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response.json"); + + CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.ADVANCED) + .getCertificateByDocumentNumber(); + + assertNotNull(response); + assertEquals(CertificateLevel.QUALIFIED, response.certificateLevel()); + assertNotNull(response.certificate()); + } + + @Test + void getCertificateByDocumentNumber_withUnknownState_throwsException() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response-unknown-state.json"); + + CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.ADVANCED); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationAuthenticationSession { + + @Test + void createNotificationAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + } + + @Test + void createNotificationAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withDocumentNumber(DOCUMENT_NUMBER) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationBasedSignatureSession { + + @Test + void createNotificationSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(64).getBytes()); + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertSessionResponse(response); + } + + @Test + void createNotificationSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(64).getBytes()); + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertSessionResponse(response); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationBasedSignatureSession { + + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + + @Test + void createLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) + .initSignatureSession(); + + assertNotNull(response); + } + + @Test + void createLinkedNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withNonce("cmFuZG9tTm9uY2U=") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) + .withShareMdClientIpAddress(true) + .initSignatureSession(); + + assertNotNull(response); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class SessionsStatus { + + @Test + void fetchFinalSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-successful-authentication.json"); + + SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); + + assertEquals("COMPLETE", status.getState()); + assertEquals("OK", status.getResult().getEndResult()); + } + + @Test + void getSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-running.json"); + + SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); + + assertEquals("RUNNING", status.getState()); + assertNull(status.getResult()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForAuth { + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInitialCallbackUrl(request.initialCallbackUrl()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.AUTHENTICATION, deviceLinkType, response.sessionToken()); + } + + @Test + void createDynamicContent_authenticationWithQRCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_authenticationWithQRCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForSignature { + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.SIGNATURE, deviceLinkType, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCodeImage() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForCertificateChoice { + + @Test + void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_createQrCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + URI deviceLinkUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withInitialCallbackUrl("https://smart-id.com/callback") + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLinkUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.sessionToken()); + } + } + + private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { + assertEquals("https", qrCodeUri.getScheme()); + assertEquals("smart-id.com", qrCodeUri.getHost()); + assertEquals("/device-link/", qrCodeUri.getPath()); + + assertTrue(qrCodeUri.getQuery().contains("version=1.0")); + assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); + assertTrue(qrCodeUri.getQuery().contains("lang=eng")); + assertTrue(qrCodeUri.getQuery().contains("authCode=")); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java index 5bfbd684..16a9f71c 100644 --- a/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.lang.reflect.AnnotatedElement; -import java.util.Optional; - -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; -import org.junit.jupiter.api.extension.ExtensionContext; - -public class SmartIdDemoCondition implements ExecutionCondition { - - /** - * Allows switching off tests going against smart-id demo env. - * This is sometimes needed if the test data in smart-id is temporarily broken. - */ - private static final boolean TEST_AGAINST_SMART_ID_DEMO = true; - - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - Optional element = context.getElement(); - if (element.isPresent() && element.get().isAnnotationPresent(SmartIdDemoIntegrationTest.class) && !TEST_AGAINST_SMART_ID_DEMO) { - return ConditionEvaluationResult.disabled("Running against Smart-ID demo is turned off"); - } - return ConditionEvaluationResult.enabled("Running against Smart-ID demo is turned on"); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SmartIdDemoCondition implements ExecutionCondition { + + /** + * Allows switching off tests going against smart-id demo env. + * This is sometimes needed if the test data in smart-id is temporarily broken. + */ + private static final boolean TEST_AGAINST_SMART_ID_DEMO = true; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional element = context.getElement(); + if (element.isPresent() && element.get().isAnnotationPresent(SmartIdDemoIntegrationTest.class) && !TEST_AGAINST_SMART_ID_DEMO) { + return ConditionEvaluationResult.disabled("Running against Smart-ID demo is turned off"); + } + return ConditionEvaluationResult.enabled("Running against Smart-ID demo is turned on"); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java index 14a5fc74..2e1439a5 100644 --- a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java @@ -1,40 +1,40 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.junit.jupiter.api.extension.ExtendWith; - -@Target({ElementType.TYPE, ElementType.METHOD}) // Can be applied to classes or methods -@Retention(RetentionPolicy.RUNTIME) -@ExtendWith(SmartIdDemoCondition.class) -public @interface SmartIdDemoIntegrationTest { -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target({ElementType.TYPE, ElementType.METHOD}) // Can be applied to classes or methods +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SmartIdDemoCondition.class) +public @interface SmartIdDemoIntegrationTest { +} diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 66b6118b..4d2fc0c8 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -1,153 +1,153 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; - -public class SmartIdRestServiceStubs { - - public static void stubNotFoundResponse(String urlEquals) { - stubFor(get(urlEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } - - public static void stubPostRequestWithResponse(String url, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubNotFoundResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 404); - } - - public static void stubUnauthorizedResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 401); - } - - public static void stubBadRequestResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 400); - } - - public static void stubForbiddenResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 403); - } - - public static void stubErrorResponse(String url, String requestFile, int errorStatus) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) - .willReturn(aResponse() - .withStatus(errorStatus) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } - - public static void stubRequestWithResponse(String urlEquals, String responseFile) { - stubFor(get(urlPathEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubStrictRequestWithResponse(String url, String requestFile, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), false, false)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { - String urlEquals = "/session/" + sessionId; - stubFor(get(urlEqualTo(urlEquals)) - .inScenario("session status") - .whenScenarioStateIs(startState) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile))) - .willSetStateTo(endState) - ); - } - - public static void stubPostErrorResponse(String url, int errorStatus) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(errorStatus) - .withHeader("Content-Type", "application/json") - .withBody(""))); - } - - private static String readFileBody(String fileName) { - ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull(resource, "File not found: " + fileName); - File file = new File(resource.getFile()); - try { - return Files.readString(file.toPath()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class SmartIdRestServiceStubs { + + public static void stubNotFoundResponse(String urlEquals) { + stubFor(get(urlEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } + + public static void stubPostRequestWithResponse(String url, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubNotFoundResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 404); + } + + public static void stubUnauthorizedResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 401); + } + + public static void stubBadRequestResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 400); + } + + public static void stubForbiddenResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 403); + } + + public static void stubErrorResponse(String url, String requestFile, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } + + public static void stubRequestWithResponse(String urlEquals, String responseFile) { + stubFor(get(urlPathEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubStrictRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), false, false)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { + String urlEquals = "/session/" + sessionId; + stubFor(get(urlEqualTo(urlEquals)) + .inScenario("session status") + .whenScenarioStateIs(startState) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile))) + .willSetStateTo(endState) + ); + } + + public static void stubPostErrorResponse(String url, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody(""))); + } + + private static String readFileBody(String fileName) { + ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + File file = new File(resource.getFile()); + try { + return Files.readString(file.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java index 47e0860d..4b7c2dee 100644 --- a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java @@ -1,48 +1,48 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; - -public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), - Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; + +public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), + Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); + } +} diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index 847e2fa7..f41876a6 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -1,83 +1,83 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - - -public class VerificationCodeCalculatorTest { - - @Test - public void calculate_ok() { - byte[] dummyDocumentHash = new byte[]{27, -69}; - String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); - assertEquals("4555", verificationCode); - } - - @ParameterizedTest - @ArgumentsSource(VerificationCodeCalculatorArgumentProvider.class) - public void calculate_generateCorrectVerificationCodes(String expectedVerificationCode, String inputString) { - byte[] hash = DigestCalculator.calculateDigest(inputString.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - assertEquals(expectedVerificationCode, VerificationCodeCalculator.calculate(hash)); - } - - @ParameterizedTest - @NullAndEmptySource - public void calculate_withEmptyInput_throwsException(byte[] data) { - var ex = assertThrows(SmartIdClientException.class, () -> VerificationCodeCalculator.calculate(data)); - assertEquals("Parameter 'data' cannot be empty", ex.getMessage()); - } - - private static class VerificationCodeCalculatorArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("7712", "Hello World!"), - Arguments.of("4612", "Hedgehogs – why can't they just share the hedge?"), - Arguments.of("7782", "Go ahead, make my day."), - Arguments.of("1464", "You're gonna need a bigger boat."), - Arguments.of("4240", "Say 'hello' to my little friend!") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + + +public class VerificationCodeCalculatorTest { + + @Test + public void calculate_ok() { + byte[] dummyDocumentHash = new byte[]{27, -69}; + String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); + assertEquals("4555", verificationCode); + } + + @ParameterizedTest + @ArgumentsSource(VerificationCodeCalculatorArgumentProvider.class) + public void calculate_generateCorrectVerificationCodes(String expectedVerificationCode, String inputString) { + byte[] hash = DigestCalculator.calculateDigest(inputString.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + assertEquals(expectedVerificationCode, VerificationCodeCalculator.calculate(hash)); + } + + @ParameterizedTest + @NullAndEmptySource + public void calculate_withEmptyInput_throwsException(byte[] data) { + var ex = assertThrows(SmartIdClientException.class, () -> VerificationCodeCalculator.calculate(data)); + assertEquals("Parameter 'data' cannot be empty", ex.getMessage()); + } + + private static class VerificationCodeCalculatorArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("7712", "Hello World!"), + Arguments.of("4612", "Hedgehogs – why can't they just share the hedge?"), + Arguments.of("7782", "Go ahead, make my day."), + Arguments.of("1464", "You're gonna need a bigger boat."), + Arguments.of("4240", "Say 'hello' to my little friend!") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java index 7f26c056..081927ed 100644 --- a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -1,167 +1,167 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.InvalidCertificateGenerator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class NonQualifiedAuthenticationCertificatePurposeValidatorTest { - - private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); - private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); - private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; - private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; - - private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; - - @BeforeEach - void setUp() { - purposeValidator = new NonQualifiedAuthenticationCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - purposeValidator.validate(NQ_AUTH_CERT); - } - - @Test - void validate_certificateNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); - assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_extendedKeyUsageIsMissing_throwException() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(null) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_invalidExtendedKeyProvided_throwException() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageIsMissing() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageNotSmartIdAuth() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .withKeyUsage(keyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - private static CertificatePolicies toNonQualifiedAuthCertificate() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_POLICY_OID), - new DERSequence() - ); - return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class NonQualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); + private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; + + private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new NonQualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + purposeValidator.validate(NQ_AUTH_CERT); + } + + @Test + void validate_certificateNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + private static CertificatePolicies toNonQualifiedAuthCertificate() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } +} diff --git a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java index 15aadb63..d35e250e 100644 --- a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -1,170 +1,170 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.InvalidCertificateGenerator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class QualifiedAuthenticationCertificatePurposeValidatorTest { - - private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt")); - private static final X509Certificate AUTH_CERT_BEFORE_APRIL_2025 = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-pnolv-020100-29990-mock-q.crt")); - private static final String SK_QUALIFIED_AUTH_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; - private static final String NCP_PLUS_POLICY_OID = "0.4.0.2042.1.2"; - - private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; - - @BeforeEach - void setUp() { - purposeValidator = new QualifiedAuthenticationCertificatePurposeValidator(); - } - - @Test - void validate_authCert_afterApril2025_ok() { - assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); - } - - // TODO - 23.09.25: Will leave it for now, as change might be needed for automated testing. - @Disabled("Test-certificate was created with 1.3.6.1.4.1.10015.3.17.2 and conflicts with required value 1.3.6.1.4.1.10015.17.2") - @Test - void validate_authCert_beforeApril2025_ok() { - assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT_BEFORE_APRIL_2025)); - } - - @Test - void validate_certificateIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); - assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); - assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - @Test - void validate_extendedKeyUsageIsMissing_throwException() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(null) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_invalidExtendedKeyProvided_throwException() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageIsMissing() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageNotSmartIdAuth() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .withKeyUsage(keyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - private static CertificatePolicies toQualifiedSmartIdAuthPolicy() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_AUTH_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_PLUS_POLICY_OID), - new DERSequence() - ); - return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class QualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt")); + private static final X509Certificate AUTH_CERT_BEFORE_APRIL_2025 = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-pnolv-020100-29990-mock-q.crt")); + private static final String SK_QUALIFIED_AUTH_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; + private static final String NCP_PLUS_POLICY_OID = "0.4.0.2042.1.2"; + + private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new QualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_authCert_afterApril2025_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); + } + + // TODO - 23.09.25: Will leave it for now, as change might be needed for automated testing. + @Disabled("Test-certificate was created with 1.3.6.1.4.1.10015.3.17.2 and conflicts with required value 1.3.6.1.4.1.10015.17.2") + @Test + void validate_authCert_beforeApril2025_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT_BEFORE_APRIL_2025)); + } + + @Test + void validate_certificateIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + private static CertificatePolicies toQualifiedSmartIdAuthPolicy() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_AUTH_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_PLUS_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } +} diff --git a/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java index 03730941..9338fdd1 100644 --- a/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java +++ b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java @@ -1,73 +1,73 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.stream.Stream; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class InteractionValidatorTest { - - @ParameterizedTest - @MethodSource("getValidDisplayTextForInteraction") - void validate_deviceLinkInteraction_ok(String displayText) { - assertDoesNotThrow(() -> InteractionValidator.validate(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); - } - - @ParameterizedTest - @MethodSource("getValidDisplayTextForInteraction") - void validate_notificationInteraction_ok(String displayText) { - assertDoesNotThrow(() -> InteractionValidator.validate(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); - } - - @ParameterizedTest - @MethodSource("getInvalidConfirmationMessageDisplayText") - void validate_interactionWithInvalidDisplayTextLength_throwException(String displayText, String expectedMessage) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> InteractionValidator.validate(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, displayText)); - assertEquals(expectedMessage, ex.getMessage()); - } - - public static Stream getValidDisplayTextForInteraction() { - return Stream.of("a", "a".repeat(60)).map(Arguments::of); - } - - public static Stream getInvalidConfirmationMessageDisplayText() { - return Stream.of(Arguments.of(null, "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), - Arguments.of("", "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), - Arguments.of("a".repeat(201), "Value for 'displayText200' must not exceed 200 characters")); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class InteractionValidatorTest { + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_deviceLinkInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_notificationInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getInvalidConfirmationMessageDisplayText") + void validate_interactionWithInvalidDisplayTextLength_throwException(String displayText, String expectedMessage) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> InteractionValidator.validate(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, displayText)); + assertEquals(expectedMessage, ex.getMessage()); + } + + public static Stream getValidDisplayTextForInteraction() { + return Stream.of("a", "a".repeat(60)).map(Arguments::of); + } + + public static Stream getInvalidConfirmationMessageDisplayText() { + return Stream.of(Arguments.of(null, "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("", "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("a".repeat(201), "Value for 'displayText200' must not exceed 200 characters")); + } +} diff --git a/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java index 286c4c54..4273ba2a 100644 --- a/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java +++ b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java @@ -1,88 +1,88 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.rest.dao.Interaction; - -class InteractionsMapperTest { - - @Test - void from_deviceLinkInteraction() { - DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_deviceLinkInteractionsList() { - DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); - - assertFalse(interactions.isEmpty()); - Interaction interaction = interactions.get(0); - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_notificationInteraction() { - NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_notificationInteractionsList() { - NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); - - assertFalse(interactions.isEmpty()); - Interaction interaction = interactions.get(0); - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.rest.dao.Interaction; + +class InteractionsMapperTest { + + @Test + void from_deviceLinkInteraction() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_deviceLinkInteractionsList() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteraction() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteractionsList() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java index 69adad3c..0b1ebe40 100644 --- a/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java @@ -1,77 +1,77 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.regex.Pattern; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class UrlSafeTokenGeneratorTest { - - @Test - void random() { - String random = UrlSafeTokenGenerator.random(); - - assertTrue(random.length() >= 22 && random.length() <= 86); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @Test - void ofLength() { - String random = UrlSafeTokenGenerator.ofLength(22); - - assertEquals(22, random.length()); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @Test - void randomBetween() { - String random = UrlSafeTokenGenerator.randomBetween(22, 24); - - assertTrue(random.length() >= 22 && random.length() <= 24); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @ParameterizedTest - @CsvSource({ - "21, 86", // min length smaller than allowed - "22, 87", // max length larger than allowed - "86, 22" // min length larger than max length - }) - void randomBetween(int minLength, int maxLength) { - var ex = assertThrows(SmartIdClientException.class, () -> UrlSafeTokenGenerator.randomBetween(minLength, maxLength)); - assertEquals("Length must be between 22 and 86 chars", ex.getMessage()); - } -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class UrlSafeTokenGeneratorTest { + + @Test + void random() { + String random = UrlSafeTokenGenerator.random(); + + assertTrue(random.length() >= 22 && random.length() <= 86); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void ofLength() { + String random = UrlSafeTokenGenerator.ofLength(22); + + assertEquals(22, random.length()); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void randomBetween() { + String random = UrlSafeTokenGenerator.randomBetween(22, 24); + + assertTrue(random.length() >= 22 && random.length() <= 24); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @ParameterizedTest + @CsvSource({ + "21, 86", // min length smaller than allowed + "22, 87", // max length larger than allowed + "86, 22" // min length larger than max length + }) + void randomBetween(int minLength, int maxLength) { + var ex = assertThrows(SmartIdClientException.class, () -> UrlSafeTokenGenerator.randomBetween(minLength, maxLength)); + assertEquals("Length must be between 22 and 86 chars", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java index b5378658..935e719e 100644 --- a/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java +++ b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java @@ -1,100 +1,100 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class DeviceLinkInteractionTest { - - @Nested - class DisplayTextAndPin { - - @Test - void displayTextAndPin_ok() { - DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPin("Log in?"); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void displayTextAndPin_textIsEmpty_throwException(String displayText) { - var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin(displayText)); - assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); - } - - @Test - void displayTextAndPin_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin("a".repeat(61))); - assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessage { - - @Test - void confirmationMessage() { - DeviceLinkInteraction interaction = DeviceLinkInteraction.confirmationMessage("Log in?"); - - assertEquals(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessage_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); - } - - @Test - void confirmationMessage_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Test - void instantiateDeviceLinkWithNullValues_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkInteraction(null, null, null)); - assertEquals("Value for 'type' must be set", ex.getMessage()); - } -} +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class DeviceLinkInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPin("Log in?"); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.confirmationMessage("Log in?"); + + assertEquals(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateDeviceLinkWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java index eb9e3874..fe1ca29e 100644 --- a/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java +++ b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java @@ -1,125 +1,125 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class NotificationInteractionTest { - - @Nested - class DisplayTextAndPin { - - @Test - void displayTextAndPin_ok() { - NotificationInteraction interaction = NotificationInteraction.displayTextAndPin("Log in?"); - - assertEquals(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void displayTextAndPin_textIsEmpty_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin(displayText)); - assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); - } - - @Test - void displayTextAndPin_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin("a".repeat(61))); - assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessage { - - @Test - void confirmationMessage_ok() { - NotificationInteraction interaction = NotificationInteraction.confirmationMessage("Log in?"); - - assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessage_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); - } - - @Test - void confirmationMessage_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessageAndVerificationCodeChoice { - - @Test - void confirmationMessageAndVerificationCodeChoice_ok() { - NotificationInteraction interaction = NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Log in?"); - - assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessageAndVerificationCodeChoice_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'", ex.getMessage()); - } - - @Test - void confirmationMessageAndVerificationCodeChoice_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Test - void instantiateNotificationInteractionWithNullValues_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new NotificationInteraction(null, null, null)); - assertEquals("Value for 'type' must be set", ex.getMessage()); - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class NotificationInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + NotificationInteraction interaction = NotificationInteraction.displayTextAndPin("Log in?"); + + assertEquals(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessage("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessageAndVerificationCodeChoice { + + @Test + void confirmationMessageAndVerificationCodeChoice_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessageAndVerificationCodeChoice_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'", ex.getMessage()); + } + + @Test + void confirmationMessageAndVerificationCodeChoice_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateNotificationInteractionWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new NotificationInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java index 4f5a2031..d0c49e07 100644 --- a/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java @@ -1,60 +1,60 @@ -package ee.sk.smartid.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -public class SemanticsIdentifierTest { - - @Test - public void constructor1() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("AAA", "BB", "C123"); - - assertThat(semanticsIdentifier.getIdentifier(), is("AAABB-C123")); - } - - @Test - public void constructor2() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, "BB", "CCC"); - - assertThat(semanticsIdentifier.getIdentifier(), is("PNOBB-CCC")); - } - - @Test - public void constructor3() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "CCC-DDDDD"); - - assertThat(semanticsIdentifier.getIdentifier(), is("PNOLV-CCC-DDDDD")); - } - -} +package ee.sk.smartid.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +public class SemanticsIdentifierTest { + + @Test + public void constructor1() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("AAA", "BB", "C123"); + + assertThat(semanticsIdentifier.getIdentifier(), is("AAABB-C123")); + } + + @Test + public void constructor2() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, "BB", "CCC"); + + assertThat(semanticsIdentifier.getIdentifier(), is("PNOBB-CCC")); + } + + @Test + public void constructor3() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "CCC-DDDDD"); + + assertThat(semanticsIdentifier.getIdentifier(), is("PNOLV-CCC-DDDDD")); + } + +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index b3619a6f..ebf7033c 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -1,949 +1,949 @@ -package ee.sk.smartid.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.time.Duration; -import java.time.Instant; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.AuthenticationCertificateLevel; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.CertificateByDocumentNumberResult; -import ee.sk.smartid.CertificateChoiceResponse; -import ee.sk.smartid.CertificateChoiceResponseValidator; -import ee.sk.smartid.CertificateLevel; -import ee.sk.smartid.CertificateValidator; -import ee.sk.smartid.CertificateValidatorImpl; -import ee.sk.smartid.DeviceLinkAuthenticationResponseValidator; -import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; -import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; -import ee.sk.smartid.DeviceLinkType; -import ee.sk.smartid.FileTrustedCAStoreBuilder; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.NotificationAuthenticationResponseValidator; -import ee.sk.smartid.NotificationAuthenticationSessionRequestBuilder; -import ee.sk.smartid.QrCodeGenerator; -import ee.sk.smartid.RpChallenge; -import ee.sk.smartid.RpChallengeGenerator; -import ee.sk.smartid.SessionType; -import ee.sk.smartid.SignableData; -import ee.sk.smartid.SignatureCertificatePurposeValidator; -import ee.sk.smartid.SignatureCertificatePurposeValidatorFactory; -import ee.sk.smartid.SignatureCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.SignatureResponse; -import ee.sk.smartid.SignatureResponseValidator; -import ee.sk.smartid.SignatureValueValidator; -import ee.sk.smartid.SignatureValueValidatorImpl; -import ee.sk.smartid.SmartIdClient; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.TrustedCACertStore; -import ee.sk.smartid.VerificationCodeCalculator; -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.CallbackUrlUtil; - -@SmartIdDemoIntegrationTest -public class ReadmeIntegrationTest { - - private static final Pattern NUMERIC_PATTERN = Pattern.compile("^[0-9]{4}$"); - - private SmartIdClient smartIdClient; - - @BeforeEach - void setUp() { - smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); - smartIdClient.setRelyingPartyName("DEMO"); - smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - - KeyStore keyStore = getKeystore(); - smartIdClient.setTrustStore(keyStore); - } - - @Disabled("Testing with device-link demo accounts is not possible at the moment") - @Nested - class DeviceLinkBasedExamples { - - @Nested - class Authentication { - - @Test - void anonymousAuthentication_withApp2App() { - // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Create initial callback URL. - // Store the url-token only on backend side. Do not expose it to the client side. - // The url-token will be used to validate the callback request received from Smart-ID API - CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); - - // Setup builder - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )) - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withLang("est") - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()) - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(sessionSecret); - - // Use the sessionId from the authentication session response to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // The session can have different states such as RUNNING or COMPLETE. - // Check that the session has completed successfully - assertEquals("COMPLETE", sessionStatus.getState()); - - // Receive callback from Smart-ID API - // Extract query parameters from the callback URL received - Map queryParameters = Map.of("value", callbackUrl.urlToken(), "sessionSecretDigest", "asdjlaksdjklf", "userChallengeVerifier", "abachdfajklsfa"); - - // Validate there is active user session in the application with matching url-token - String tokenInUrl = queryParameters.get("value"); - - // Validate that sessionSecretDigest in the callback URL validates against sessionSecret from the init session response - CallbackUrlUtil.validateSessionSecretDigest(queryParameters.get("sessionSecretDigest"), sessionSecret); - - // Set up AuthenticationResponseValidator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate( - sessionStatus, - builder.getAuthenticationSessionRequest(), - queryParameters.get("userChallengeVerifier"), - "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withSemanticIdentifierAndQrCode() { - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // For security reasons a new rpChallenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withElapsedSeconds(elapsedSeconds) - .withInteractions(authenticationSessionRequest.interactions()) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETED", sessionStatus.getState()); - - // Validate the response and return user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withDocumentNumberAndQrCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // For security reasons a new rpChallenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication session status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get AuthenticationSessionRequest after the request is made and store for validations - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - String sessionId = authenticationSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - - // Generate the base (unprotected) device link URI, which does not yet include the authCode - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withInteractions(authenticationSessionRequest.interactions()) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETE", sessionStatus.getState()); - - // Validate the certificate and signature, then map the authentication response to the user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - } - - @Nested - class Signature { - - @Test - void signature_withDocumentNumberAndQRCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - - // Build the device link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); - var deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withInteractions(signatureInteractions); - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); - // Get SignatureSessionRequest after the request is made and store for validations - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.sessionID(); - String sessionToken = signatureSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.sessionSecret(); - Instant receivedAt = signatureSessionResponse.receivedAt(); - URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); - - // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .withInteractions(deviceLinkSignatureSessionRequest.interactions()) - .buildDeviceLink(sessionSecret); - - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - // Validate signature response - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - // Validate signature value - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - - @Test - void signature_withSemanticIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // Build the device link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); - DeviceLinkSignatureSessionRequestBuilder deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(signatureInteractions); - - // Init signature session - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); - // Get SignatureSessionRequest after the request is made and store for validations - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.sessionID(); - String sessionToken = signatureSessionResponse.sessionToken(); - - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.sessionSecret(); - Instant receivedAt = signatureSessionResponse.receivedAt(); - - // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .withInteractions(deviceLinkSignatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request - .buildDeviceLink(sessionSecret); - // Display QR-code to the user - - // Get the session status poller - poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - // Validate signature response - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - // Validate signature value - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), - signableData.calculateHash(), - certificateChoiceResponse.getCertificate(), - signatureResponse.getRsaSsaPssParameters()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - } - } - - @Nested - class NotificationBasedExamples { - - @Test - void authentication_withDocumentNumber() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // For security reasons a new rpChallenge must be created for each new authentication request - RpChallenge rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Generate verification code to be displayed to the user - String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - - NotificationAuthenticationSessionRequestBuilder builder = smartIdClient - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))); - // Init authentication session - NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get notification-based authentication session request used for starting the authentication session - // and use it later to validate sessions status response - NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // SessionID is used to query sessions status later - String sessionId = authenticationSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Use sessionID from current session response to poll for session status - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - - // validate the sessions status and return user's identity - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = - NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withSemanticIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // For security reasons a new RpChallenge must be created for each new authentication request - RpChallenge rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Generate verification code to be displayed to the user - String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - - NotificationAuthenticationSessionRequestBuilder builder = smartIdClient.createNotificationAuthentication() - .withSemanticsIdentifier(semanticIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))); - - // Init authentication session - NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get notification-based authentication session request used for starting the authentication session - // and use it later to validate sessions status response - NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // SessionID is used to query sessions status later - String sessionId = authenticationSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Use sessionID from current session response to poll for session status - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = - NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void certificateChoice_withSemanticIdentifier() { - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // Use requested certificate level to validate certificate choice session status OK response. - CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(requestedCertificateLevel) - .initCertificateChoice(); - - String sessionId = certificateChoiceSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus sessionStatus = poller.getSessionStatus(sessionId); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); - assertNotNull(response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void signature_withSemanticsIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - CertificateLevel certificateLevel = CertificateLevel.QSCD; - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(certificateLevel) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - // SessionID is used to query sessions status later - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus, certificateLevel); - // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - // Create the Semantics Identifier - var semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" - ); - - NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(certificateLevel) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the document")) - ) - .initSignatureSession(); - - // Get the session ID and continue to querying session status - String sessionID = signatureSessionResponse.sessionID(); - - // Display verification code to the user - String verificationCode = signatureSessionResponse.vc().value(); - assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); - - // Get sessionID from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOEE-40504040001-DEMO-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); - assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - - @Test - void signature_withDocumentNumber() { - String documentNumber = "PNOEE-40504040001-DEMO-Q"; - - CertificateLevel certificateLevel = CertificateLevel.QSCD; - // Query the certificate by document number to be used for creating the DataToSign - CertificateByDocumentNumberResult certificateByDocumentNumber = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .withCertificateLevel(certificateLevel) - .getCertificateByDocumentNumber(); - - // Set up the certificate validator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - // Validate the certificate is trusted and active - certificateValidator.validate(certificateByDocumentNumber.certificate()); - - // Validate the certificate is suitable for signing - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); - SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateByDocumentNumber.certificateLevel()); - certificatePurposeValidator.validate(certificateByDocumentNumber.certificate()); - - // For example use digidoc4j with SignatureBuilder to create DataToSign using `certificateByDocumentNumber.certificate()` - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(certificateLevel) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the document")) - ) - .initSignatureSession(); - - // Get the session ID and continue to querying session status - String signatureSessionId = signatureSessionResponse.sessionID(); - - // Display verification code to the user - String verificationCode = signatureSessionResponse.vc().value(); - assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Get sessionID from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals(documentNumber, signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); - assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - } - - @Nested - class CertificateByDocumentNumberExamples { - - @Test - void queryCertificate() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // Build the certificate by document number request and query the certificate - CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - - // Set up the certificate validator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - - // Validate the certificate - certificateValidator.validate(certResponse.certificate()); - - // Validate the certificate is suitable for signing - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); - SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certResponse.certificateLevel()); - certificatePurposeValidator.validate(certResponse.certificate()); - } - } - - @Disabled("Testing with device-link demo accounts is not possible at the moment") - @Nested - class LinkedNotificationBasedSignatureSession { - - @Test - void signing_withQrCode() { - DeviceLinkSessionResponse certificateChoiceSessionResponse = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .initCertificateChoice(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Use sessionID to start polling for session status - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = certificateChoiceSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); - URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link and in authCode - Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - - // Build the device link URI - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(sessionToken) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for certificate choice session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus certificateSessionStatus = poller.fetchFinalSessionStatus(certificateChoiceSessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETED", certificateSessionStatus.getState()); - - // Validate the certificate choice response - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().build()); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - - // Start the linked notification signature session using the sessionID from the certificate choice session - LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) - .withLinkedSessionID(certificateChoiceSessionId) - .withSignableData(signableData) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign it!"))) - .initSignatureSession(); - - // Use sessionId to poll for signature session status updates - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionResponse.sessionID()); - assertEquals("COMPLETED", signatureSessionStatus.getState()); - - // Validate signature response - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - - assertNotNull(signatureResponse.getSignatureValue()); - } - } - - private static KeyStore getKeystore() { - try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(is, "changeit".toCharArray()); - return keyStore; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - throw new RuntimeException("Cannot find demo truststore", e); - } - } -} +package ee.sk.smartid.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.AuthenticationCertificateLevel; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.CertificateByDocumentNumberResult; +import ee.sk.smartid.CertificateChoiceResponse; +import ee.sk.smartid.CertificateChoiceResponseValidator; +import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.CertificateValidator; +import ee.sk.smartid.CertificateValidatorImpl; +import ee.sk.smartid.DeviceLinkAuthenticationResponseValidator; +import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkType; +import ee.sk.smartid.FileTrustedCAStoreBuilder; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.NotificationAuthenticationResponseValidator; +import ee.sk.smartid.NotificationAuthenticationSessionRequestBuilder; +import ee.sk.smartid.QrCodeGenerator; +import ee.sk.smartid.RpChallenge; +import ee.sk.smartid.RpChallengeGenerator; +import ee.sk.smartid.SessionType; +import ee.sk.smartid.SignableData; +import ee.sk.smartid.SignatureCertificatePurposeValidator; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactory; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.SignatureResponse; +import ee.sk.smartid.SignatureResponseValidator; +import ee.sk.smartid.SignatureValueValidator; +import ee.sk.smartid.SignatureValueValidatorImpl; +import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.TrustedCACertStore; +import ee.sk.smartid.VerificationCodeCalculator; +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.CallbackUrlUtil; + +@SmartIdDemoIntegrationTest +public class ReadmeIntegrationTest { + + private static final Pattern NUMERIC_PATTERN = Pattern.compile("^[0-9]{4}$"); + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + KeyStore keyStore = getKeystore(); + smartIdClient.setTrustStore(keyStore); + } + + @Disabled("Testing with device-link demo accounts is not possible at the moment") + @Nested + class DeviceLinkBasedExamples { + + @Nested + class Authentication { + + @Test + void anonymousAuthentication_withApp2App() { + // For security reasons a new hash value must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Create initial callback URL. + // Store the url-token only on backend side. Do not expose it to the client side. + // The url-token will be used to validate the callback request received from Smart-ID API + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + + // Setup builder + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link and in authCode + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withLang("est") + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()) + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(sessionSecret); + + // Use the sessionId from the authentication session response to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // The session can have different states such as RUNNING or COMPLETE. + // Check that the session has completed successfully + assertEquals("COMPLETE", sessionStatus.getState()); + + // Receive callback from Smart-ID API + // Extract query parameters from the callback URL received + Map queryParameters = Map.of("value", callbackUrl.urlToken(), "sessionSecretDigest", "asdjlaksdjklf", "userChallengeVerifier", "abachdfajklsfa"); + + // Validate there is active user session in the application with matching url-token + String tokenInUrl = queryParameters.get("value"); + + // Validate that sessionSecretDigest in the callback URL validates against sessionSecret from the init session response + CallbackUrlUtil.validateSessionSecretDigest(queryParameters.get("sessionSecretDigest"), sessionSecret); + + // Set up AuthenticationResponseValidator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate( + sessionStatus, + builder.getAuthenticationSessionRequest(), + queryParameters.get("userChallengeVerifier"), + "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifierAndQrCode() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", sessionStatus.getState()); + + // Validate the response and return user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withDocumentNumberAndQrCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication session status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get AuthenticationSessionRequest after the request is made and store for validations + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + String sessionId = authenticationSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + + // Generate the base (unprotected) device link URI, which does not yet include the authCode + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETE", sessionStatus.getState()); + + // Validate the certificate and signature, then map the authentication response to the user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + } + + @Nested + class Signature { + + @Test + void signature_withDocumentNumberAndQRCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + var deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withInteractions(signatureInteractions); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); + URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); + + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + // Validate signature response + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + // Validate signature value + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticIdentifier) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + DeviceLinkSignatureSessionRequestBuilder deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(signatureInteractions); + + // Init signature session + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); + + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); + + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request + .buildDeviceLink(sessionSecret); + // Display QR-code to the user + + // Get the session status poller + poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + // Validate signature value + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certificateChoiceResponse.getCertificate(), + signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + } + + @Nested + class NotificationBasedExamples { + + @Test + void authentication_withDocumentNumber() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new rpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))); + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // SessionID is used to query sessions status later + String sessionId = authenticationSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Use sessionID from current session response to poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + + // validate the sessions status and return user's identity + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new RpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient.createNotificationAuthentication() + .withSemanticsIdentifier(semanticIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))); + + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // SessionID is used to query sessions status later + String sessionId = authenticationSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Use sessionID from current session response to poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void certificateChoice_withSemanticIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Use requested certificate level to validate certificate choice session status OK response. + CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(requestedCertificateLevel) + .initCertificateChoice(); + + String sessionId = certificateChoiceSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus sessionStatus = poller.getSessionStatus(sessionId); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); + assertNotNull(response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void signature_withSemanticsIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + CertificateLevel certificateLevel = CertificateLevel.QSCD; + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticIdentifier) + .withCertificateLevel(certificateLevel) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + // SessionID is used to query sessions status later + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus, certificateLevel); + // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + // Create the Semantics Identifier + var semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" + ); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(certificateLevel) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the document")) + ) + .initSignatureSession(); + + // Get the session ID and continue to querying session status + String sessionID = signatureSessionResponse.sessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOEE-40504040001-DEMO-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withDocumentNumber() { + String documentNumber = "PNOEE-40504040001-DEMO-Q"; + + CertificateLevel certificateLevel = CertificateLevel.QSCD; + // Query the certificate by document number to be used for creating the DataToSign + CertificateByDocumentNumberResult certificateByDocumentNumber = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .withCertificateLevel(certificateLevel) + .getCertificateByDocumentNumber(); + + // Set up the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + // Validate the certificate is trusted and active + certificateValidator.validate(certificateByDocumentNumber.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateByDocumentNumber.certificateLevel()); + certificatePurposeValidator.validate(certificateByDocumentNumber.certificate()); + + // For example use digidoc4j with SignatureBuilder to create DataToSign using `certificateByDocumentNumber.certificate()` + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(certificateLevel) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the document")) + ) + .initSignatureSession(); + + // Get the session ID and continue to querying session status + String signatureSessionId = signatureSessionResponse.sessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals(documentNumber, signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + + @Nested + class CertificateByDocumentNumberExamples { + + @Test + void queryCertificate() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // Build the certificate by document number request and query the certificate + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // Set up the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + + // Validate the certificate + certificateValidator.validate(certResponse.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certResponse.certificateLevel()); + certificatePurposeValidator.validate(certResponse.certificate()); + } + } + + @Disabled("Testing with device-link demo accounts is not possible at the moment") + @Nested + class LinkedNotificationBasedSignatureSession { + + @Test + void signing_withQrCode() { + DeviceLinkSessionResponse certificateChoiceSessionResponse = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .initCertificateChoice(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Use sessionID to start polling for session status + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = certificateChoiceSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); + URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link and in authCode + Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + + // Build the device link URI + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(sessionToken) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for certificate choice session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus certificateSessionStatus = poller.fetchFinalSessionStatus(certificateChoiceSessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", certificateSessionStatus.getState()); + + // Validate the certificate choice response + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().build()); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + + // Start the linked notification signature session using the sessionID from the certificate choice session + LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionId) + .withSignableData(signableData) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign it!"))) + .initSignatureSession(); + + // Use sessionId to poll for signature session status updates + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionResponse.sessionID()); + assertEquals("COMPLETED", signatureSessionStatus.getState()); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + + assertNotNull(signatureResponse.getSignatureValue()); + } + } + + private static KeyStore getKeystore() { + try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, "changeit".toCharArray()); + return keyStore; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot find demo truststore", e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 69ba46f4..21698195 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -1,332 +1,332 @@ -package ee.sk.smartid.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; -import java.util.regex.Pattern; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.RpChallengeGenerator; -import ee.sk.smartid.SignatureAlgorithm; -import ee.sk.smartid.SignatureProtocol; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.VerificationCodeType; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; - -@SmartIdDemoIntegrationTest -class SmartIdRestIntegrationTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - - private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); - private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9]{4}$"); - private static final Pattern SESSION_TOKEN_PATTERN = Pattern.compile("^[A-Za-z0-9]{24}$"); - private static final Pattern SESSION_SECRET_PATTERN = Pattern.compile("^[A-Za-z0-9+/]{24}$"); - - private SmartIdConnector smartIdConnector; - - @BeforeEach - void setUp() { - smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); - } - - @Disabled("Testing device-link flows with demo accounts is not yet possible") - @Nested - class DeviceLink { - - @Nested - class Authentication { - - @Test - void initAnonymousDeviceLinkAuthentication() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkAuthentication_withDocumentNumber() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkAuthentication_withSemanticsIdentifier() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { - var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate().toBase64EncodedValue(), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new DeviceLinkAuthenticationSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.ACSP_V2, - signatureParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - null, - null, - null); - } - } - - @Nested - class CertificateChoice { - - @Test - void initDeviceLinkCertificateChoice() { - var request = new DeviceLinkCertificateChoiceSessionRequest( - RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - null, - null, - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.deviceLinkBase()); - assertNotNull(sessionResponse.receivedAt()); - } - } - - @Nested - class Signature { - - @Test - void initDeviceLinkSignature_withSemanticIdentifier() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkSignature_withDocumentNumber() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - } - } - - @Nested - class NotificationBasedRequests { - - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-40504040001"); - private static final String DOCUMENT_NUMBER = "PNOEE-40504040001-DEMO-Q"; - - @Nested - class Authentication { - - @Test - void initNotificationAuthentication_withSemanticIdentifier() { - var request = toAuthenticationRequest(); - - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - - @Test - void initNotificationAuthentication_withDocumentNumber() { - var request = toAuthenticationRequest(); - - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, DOCUMENT_NUMBER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - - private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { - var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate().toBase64EncodedValue(), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.ACSP_V2.name(), - signatureParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - new RequestProperties(true), - null, - VerificationCodeType.NUMERIC4.getValue() - ); - } - } - - @Nested - class CertificateChoice { - - @Test - void initNotificationCertificateChoice_withSemanticIdentifier() { - var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); - - NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - } - - @Nested - class Signature { - - @Test - void initNotificationSignature_withSemanticIdentifier() { - var request = toSignatureSessionRequest(); - - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); - assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); - } - - @Test - void initNotificationCertificateChoice_withDocumentNumber() { - var request = toSignatureSessionRequest(); - - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); - assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); - } - - private static NotificationSignatureSessionRequest toSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new NotificationSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null - ); - } - } - } -} +package ee.sk.smartid.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.regex.Pattern; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.RpChallengeGenerator; +import ee.sk.smartid.SignatureAlgorithm; +import ee.sk.smartid.SignatureProtocol; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.VerificationCodeType; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; + +@SmartIdDemoIntegrationTest +class SmartIdRestIntegrationTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + + private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9]{4}$"); + private static final Pattern SESSION_TOKEN_PATTERN = Pattern.compile("^[A-Za-z0-9]{24}$"); + private static final Pattern SESSION_SECRET_PATTERN = Pattern.compile("^[A-Za-z0-9+/]{24}$"); + + private SmartIdConnector smartIdConnector; + + @BeforeEach + void setUp() { + smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); + } + + @Disabled("Testing device-link flows with demo accounts is not yet possible") + @Nested + class DeviceLink { + + @Nested + class Authentication { + + @Test + void initAnonymousDeviceLinkAuthentication() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkAuthentication_withDocumentNumber() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkAuthentication_withSemanticsIdentifier() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate().toBase64EncodedValue(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new DeviceLinkAuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + null, + null, + null); + } + } + + @Nested + class CertificateChoice { + + @Test + void initDeviceLinkCertificateChoice() { + var request = new DeviceLinkCertificateChoiceSessionRequest( + RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + null, + null, + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.deviceLinkBase()); + assertNotNull(sessionResponse.receivedAt()); + } + } + + @Nested + class Signature { + + @Test + void initDeviceLinkSignature_withSemanticIdentifier() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkSignature_withDocumentNumber() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + } + } + + @Nested + class NotificationBasedRequests { + + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-40504040001"); + private static final String DOCUMENT_NUMBER = "PNOEE-40504040001-DEMO-Q"; + + @Nested + class Authentication { + + @Test + void initNotificationAuthentication_withSemanticIdentifier() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + + @Test + void initNotificationAuthentication_withDocumentNumber() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, DOCUMENT_NUMBER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + + private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate().toBase64EncodedValue(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2.name(), + signatureParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + new RequestProperties(true), + null, + VerificationCodeType.NUMERIC4.getValue() + ); + } + } + + @Nested + class CertificateChoice { + + @Test + void initNotificationCertificateChoice_withSemanticIdentifier() { + var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); + + NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + } + + @Nested + class Signature { + + @Test + void initNotificationSignature_withSemanticIdentifier() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); + } + + @Test + void initNotificationCertificateChoice_withDocumentNumber() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); + } + + private static NotificationSignatureSessionRequest toSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + return new NotificationSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null + ); + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java index b50ab0a9..6f4970d1 100644 --- a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java @@ -1,83 +1,83 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.SessionStatus; - -class SessionStatusPollerTest { - - private SmartIdConnector smartIdConnector; - - private SessionStatusPoller poller; - - @BeforeEach - void setUp() { - smartIdConnector = mock(SmartIdConnector.class); - poller = new SessionStatusPoller(smartIdConnector); - } - - @Test - void fetchFinalSessionStatus() { - SessionStatus runningStatus = new SessionStatus(); - runningStatus.setState("RUNNING"); - - SessionStatus completedStatus = new SessionStatus(); - completedStatus.setState("COMPLETE"); - - when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")) - .thenReturn(runningStatus, completedStatus); - - SessionStatus finalSessionStatus = poller.fetchFinalSessionStatus("00000000-0000-0000-0000-000000000000"); - - verify(smartIdConnector, times(2)).getSessionStatus("00000000-0000-0000-0000-000000000000"); - assertEquals("COMPLETE", finalSessionStatus.getState()); - } - - @Test - void getSessionStatus() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("RUNNING"); - when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); - - SessionStatus sessionsStatus = poller.getSessionStatus("00000000-0000-0000-0000-000000000000"); - - assertEquals("RUNNING", sessionsStatus.getState()); - assertNull(sessionsStatus.getResult()); - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.SessionStatus; + +class SessionStatusPollerTest { + + private SmartIdConnector smartIdConnector; + + private SessionStatusPoller poller; + + @BeforeEach + void setUp() { + smartIdConnector = mock(SmartIdConnector.class); + poller = new SessionStatusPoller(smartIdConnector); + } + + @Test + void fetchFinalSessionStatus() { + SessionStatus runningStatus = new SessionStatus(); + runningStatus.setState("RUNNING"); + + SessionStatus completedStatus = new SessionStatus(); + completedStatus.setState("COMPLETE"); + + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")) + .thenReturn(runningStatus, completedStatus); + + SessionStatus finalSessionStatus = poller.fetchFinalSessionStatus("00000000-0000-0000-0000-000000000000"); + + verify(smartIdConnector, times(2)).getSessionStatus("00000000-0000-0000-0000-000000000000"); + assertEquals("COMPLETE", finalSessionStatus.getState()); + } + + @Test + void getSessionStatus() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("RUNNING"); + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); + + SessionStatus sessionsStatus = poller.getSessionStatus("00000000-0000-0000-0000-000000000000"); + + assertEquals("RUNNING", sessionsStatus.getState()); + assertNull(sessionsStatus.getResult()); + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 0082e0ee..8e5d7ce0 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -1,1779 +1,1779 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.containing; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubStrictRequestWithResponse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.StringStartsWith.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.CertificateLevel; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.SignatureProtocol; -import ee.sk.smartid.SmartIdRestServiceStubs; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.VerificationCode; -import ee.sk.smartid.util.InteractionUtil; - -class SmartIdRestConnectorTest { - - private static final String SESSION_SECRET = "c2Vzc2lvblNlY3JldA=="; - - @Nested - @WireMockTest(httpPort = 18089) - class SessionStatusTests { - - private static final String SERVER_RANDOM = "J0iyCYOu8cTWuoD8rD05IIrZ"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void getSessionStatus_running() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - } - - @Test - void getSessionStatus_running_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running-with-ignored-properties.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - assertNotNull(sessionStatus.getIgnoredProperties()); - assertEquals(2, sessionStatus.getIgnoredProperties().length); - assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); - assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); - } - - @Test - void getSessionStatus_forSuccessfulAuthenticationRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); - assertSuccessfulResponse(sessionStatus); - assertEquals("displayTextAndPIN", sessionStatus.getInteractionTypeUsed()); - - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - SessionSignature sessionSignature = sessionStatus.getSignature(); - assertNotNull(sessionSignature); - assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionSignature.getValue())); - assertEquals(SERVER_RANDOM, sessionSignature.getServerRandom()); - assertTrue(Pattern.matches("^[a-zA-Z0-9-_]{43}$", sessionSignature.getUserChallenge())); - assertEquals("QR", sessionSignature.getFlowType()); - assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); - - assertSignatureAlgorithmParameters(sessionSignature, "SHA3-512"); - - assertNotNull(sessionStatus.getCert()); - assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-certificate-choice.json"); - assertSuccessfulResponse(sessionStatus); - - assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_forSuccessfulSignatureRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); - assertSuccessfulResponse(sessionStatus); - assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); - - assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); - SessionSignature sessionSignature = sessionStatus.getSignature(); - assertNotNull(sessionSignature); - assertThat(sessionSignature.getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); - assertEquals("QR", sessionSignature.getFlowType()); - assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); - - assertSignatureAlgorithmParameters(sessionSignature, "SHA-512"); - - assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); - assertSuccessfulResponse(sessionStatus); - - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/session-status-successful-authentication.json"); - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - assertSuccessfulResponse(sessionStatus); - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); - } - - @Test - void getSessionStatus_whenSessionNotFound() { - assertThrows(SessionNotFoundException.class, () -> { - stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - }); - } - - @Nested - class UserRefusedInteractions { - - @Test - void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("confirmationMessage", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("confirmationMessageAndVerificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("displayTextAndPIN", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("verificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); - } - } - - @Test - void getSessionStatus_userHasRefusedCertChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_CERT_CHOICE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("TIMEOUT", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("WRONG_VC", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_documentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("DOCUMENT_UNUSABLE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_protocolFailure() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-protocol-failure.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("PROTOCOL_FAILURE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_expectedLinkedSession() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-expected-linked-session.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("EXPECTED_LINKED_SESSION", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_serverError() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-server-error.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_accountUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-account-unusable.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("ACCOUNT_UNUSABLE", sessionStatus.getResult().getEndResult()); - } - - private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); - return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } - - private static void assertSuccessfulResponse(SessionStatus sessionStatus) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertNotNull(sessionStatus.getResult()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); - } - - private static void assertSignatureAlgorithmParameters(SessionSignature sessionSignature, String expectedHashAlgorithm) { - SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - assertEquals(expectedHashAlgorithm, signatureAlgorithmParameters.getHashAlgorithm()); - var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); - assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); - SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); - assertEquals(expectedHashAlgorithm, parameters.getHashAlgorithm()); - assertEquals(64, signatureAlgorithmParameters.getSaltLength()); - assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); - } - } - - @Nested - @WireMockTest(httpPort = 18082) - class SemanticsIdentifierDeviceLinkAuthentication { - - private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-30303039914"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18082"); - } - - @Test - void initDeviceLinkAuthentication_qrCodeFlow_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toQrAuthenticationSessionRequest(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_sameDeviceOnlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(null, "https://example.com/callback"); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_sameDeviceAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(new RequestProperties(true), "https://example.com/callback"); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_accountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18083) - class DocumentNumberDeviceLinkAuthentication { - - private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18083"); - } - - @Test - void initDeviceLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), "PNOEE-48010010101-MOCK-Q")); - } - - @Test - void initDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - } - - @Nested - @WireMockTest(httpPort = 18081) - class AnonymousDeviceLinkAuthentication { - - private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/device-link/anonymous"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18081"); - } - - @Test - void initAnonymousDeviceLinkAuthentication_qrCodeFlow_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initAnonymousDeviceLinkAuthentication_sameDeviceFlow_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initAnonymousDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - } - - @Nested - @WireMockTest(httpPort = 18082) - class SemanticsIdentifierNotificationAuthentication { - - private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18082"); - } - - @Test - void initNotificationAuthentication_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/notification/notification-authentication-session-request-all-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), SEMANTICS_IDENTIFIER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); - - assertThrows(SmartIdClientException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18083) - class DocumentNumberNotificationAuthentication { - - private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18083"); - } - - @Test - void initNotificationAuthentication_onlyRequeriedFields_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_allFields_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/notification/notification-authentication-session-request-all-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), DOCUMENT_NUMBER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); - - var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(authenticationRequest, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkCertificateChoiceTests { - - private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/signature/certificate-choice/device-link/anonymous"; - - private SmartIdConnector connector; - - @BeforeEach - public void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initDeviceLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); - Instant end = Instant.now(); - - assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); - } - - @Test - void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_userAccountNotFound() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_invalidRequest() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - - var request = new DeviceLinkCertificateChoiceSessionRequest("", "", null, null, null, null, null); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); - } - - @Test - void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); - } - - @Test - void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - private static DeviceLinkCertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { - return new DeviceLinkCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - "ADVANCED", - null, - null, - null, - null - ); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class LinkedNotificationSignature { - - private static final String LINKED_SIGNATURE_PATH = "/signature/notification/linked/PNOEE-31111111111-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-31111111111-MOCK-Q"; - private static final String NONCE = "cmFuZG9tTm9uY2U="; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initLinkedNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionRequest request = toLinkedSignatureSessionRequest(null, null, null); - LinkedSignatureSessionResponse linkedSignatureSessionResponse = connector.initLinkedNotificationSignature(request, DOCUMENT_NUMBER); - - assertNotNull(linkedSignatureSessionResponse); - assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); - } - - @Test - void initLinkedNotificationSignature_withAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse linkedSignatureSessionResponse = - connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER); - - assertNotNull(linkedSignatureSessionResponse); - assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); - } - - @Test - void initLinkedNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_accountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - var linkedSignatureSessionRequest = toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, "cmFuZG9tTm9uY2U=", new RequestProperties(true)); - connector.initLinkedNotificationSignature(linkedSignatureSessionRequest, DOCUMENT_NUMBER); - }); - } - - @Test - void initLinkedNotificationSignature_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - private LinkedSignatureSessionRequest toFullLinkedSignatureSessionRequest() { - return toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, NONCE, new RequestProperties(true)); - } - - private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(CertificateLevel certificateLevel, - String nonce, - RequestProperties requestProperties) { - var rawDigestSignatureProtocolParameters = new RawDigestSignatureProtocolParameters( - "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); - return new LinkedSignatureSessionRequest("00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel != null ? certificateLevel.name() : null, - "RAW_DIGEST_SIGNATURE", - rawDigestSignatureProtocolParameters, - "10000000-0000-000-000-000000000000", - nonce, - "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", - requestProperties, - null); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class SemanticsIdentifierNotificationCertificateChoiceTests { - - private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/signature/certificate-choice/notification/etsi/PNOEE-31111111111"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); - - private SmartIdRestConnector connector; - - @BeforeEach - public void setUp() { - WireMock.configureFor("localhost", 18089); - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initCertificateChoice_onlyRequiredFields_successful() { - stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); - - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @Test - void initCertificateChoice_allFields_successful() { - stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - var request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - "QUALIFIED", - "cmFuZG9tTm9uY2U=", - null, - new RequestProperties(true)); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); - - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @Test - void initCertificateChoice_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - null, - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_rpDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 471); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_userShouldCheckPortal_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 472); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_javaClientBeingUsedIsTooOld_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 480); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 580); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18086) - class CertificateByDocumentNumberTests { - - private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18086"); - } - - @Test - void getCertificateByDocumentNumber_successful() { - SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); - - CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest()); - - assertNotNull(response); - assertEquals("OK", response.state()); - assertNotNull(response.cert()); - assertEquals("QUALIFIED", response.cert().certificateLevel()); - assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { - SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); - - var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", null); - CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); - - assertNotNull(response); - assertEquals("OK", response.state()); - assertNotNull(response.cert()); - assertEquals("QUALIFIED", response.cert().certificateLevel()); - assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); - } - - @Test - void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(UserAccountNotFoundException.class, - () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); - } - - @Test - void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkSignatureTests { - - private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - private SmartIdRestConnector connector; - - @BeforeEach - public void setUp() { - WireMock.configureFor("localhost", 18089); - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initDeviceLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); - - assertNotNull(response); - assertEquals("test-session-id", response.sessionID()); - assertEquals("test-session-token", response.sessionToken()); - assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); - } - - @Test - void initDeviceLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - String documentNumber = "PNOEE-31111111111-MOCK-Q"; - - DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); - - assertNotNull(response); - assertEquals("test-session-id", response.sessionID()); - assertEquals("test-session-token", response.sessionToken()); - assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); - } - - @Test - void initDeviceLinkSignature_userAccountNotFound() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_relyingPartyNoPermission() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_invalidRequest() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - - assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); - } - - @Test - void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsSmartIdClientException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); - } - - @Test - void initDeviceLinkSignature_throwsServerMaintenanceException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18084) - class SemanticsIdentifierNotificationSignature { - - private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18084"); - } - - @Test - void initNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/signature/notification-signature-session-request-all-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("DEMO", CertificateLevel.QSCD, "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", true); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - }); - } - - @Test - void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); - } - - @Test - void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18085) - class DocumentNumberNotificationSignature { - - private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18085"); - } - - @Test - void initNotificationSignature_onlyRequiredFields() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_allFields() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("NOT DEMO", null, null, null); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); - } - - @Test - void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - } - - private DeviceLinkAuthenticationSessionRequest toQrAuthenticationSessionRequest() { - return toDeviceLinkAuthenticationSessionRequest(null, null); - } - - private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest(RequestProperties requestProperties, - String initialCallbackUrl) { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( - Base64.toBase64String("a".repeat(32).getBytes()), - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new DeviceLinkAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - CertificateLevel.QUALIFIED.name(), - SignatureProtocol.ACSP_V2, - signatureProtocolParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - requestProperties, - null, - initialCallbackUrl - ); - } - - private static NotificationAuthenticationSessionRequest toNotificationAuthenticationSessionRequest(CertificateLevel certificateLevel, RequestProperties requestProperties) { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( - Base64.toBase64String("a".repeat(32).getBytes()), - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2.name(), - signatureProtocolParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Login?"))), - requestProperties, - null, - "numeric4" - ); - } - - private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { - return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); - } - - private static DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { - var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", - "rsassa-pss", - new SignatureAlgorithmParameters("SHA3-512")); - - return new DeviceLinkSignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", - "BANK123", - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - protocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign the document", null))), - null, - null); - } - - private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest() { - return toNotificationSignatureSessionRequest("DEMO", null, null, null); - } - - private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest(String relyingPartyName, - CertificateLevel certificateLevel, - String nonce, - Boolean shareIpAddress) { - var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "rsassa-pss", - new SignatureAlgorithmParameters("SHA-512")); - var interaction = new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Sign it!"); - return new NotificationSignatureSessionRequest("00000000-0000-4000-8000-000000000000", - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - protocolParameters, - nonce, - null, - InteractionUtil.encodeToBase64(List.of(interaction)), - shareIpAddress != null ? new RequestProperties(shareIpAddress) : null); - } - - private static void assertResponseValues(DeviceLinkSessionResponse response, - String expectedSessionToken, - String expectedSessionSecret, - Instant start, - Instant end) { - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - assertEquals(expectedSessionToken, response.sessionToken()); - assertEquals(expectedSessionSecret, response.sessionSecret()); - assertNotNull(response.receivedAt()); - assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); - assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse response) { - assertNotNull(response); - assertNotNull(response.sessionID()); - VerificationCode verificationCode = response.vc(); - assertNotNull(verificationCode); - assertNotNull(verificationCode.type()); - assertNotNull(verificationCode.value()); - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubStrictRequestWithResponse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.SignatureProtocol; +import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.InteractionUtil; + +class SmartIdRestConnectorTest { + + private static final String SESSION_SECRET = "c2Vzc2lvblNlY3JldA=="; + + @Nested + @WireMockTest(httpPort = 18089) + class SessionStatusTests { + + private static final String SERVER_RANDOM = "J0iyCYOu8cTWuoD8rD05IIrZ"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void getSessionStatus_running() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + } + + @Test + void getSessionStatus_running_withIgnoredProperties() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running-with-ignored-properties.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + assertNotNull(sessionStatus.getIgnoredProperties()); + assertEquals(2, sessionStatus.getIgnoredProperties().length); + assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); + assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); + } + + @Test + void getSessionStatus_forSuccessfulAuthenticationRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("displayTextAndPIN", sessionStatus.getInteractionTypeUsed()); + + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionSignature.getValue())); + assertEquals(SERVER_RANDOM, sessionSignature.getServerRandom()); + assertTrue(Pattern.matches("^[a-zA-Z0-9-_]{43}$", sessionSignature.getUserChallenge())); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + assertSignatureAlgorithmParameters(sessionSignature, "SHA3-512"); + + assertNotNull(sessionStatus.getCert()); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_forSuccessfulCertificateRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-certificate-choice.json"); + assertSuccessfulResponse(sessionStatus); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_forSuccessfulSignatureRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); + + assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertThat(sessionSignature.getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + assertSignatureAlgorithmParameters(sessionSignature, "SHA-512"); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_hasUserAgentHeader() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); + assertSuccessfulResponse(sessionStatus); + + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + void getSessionStatus_withTimeoutParameter() { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/session-status-successful-authentication.json"); + connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); + SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + assertSuccessfulResponse(sessionStatus); + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); + } + + @Test + void getSessionStatus_whenSessionNotFound() { + assertThrows(SessionNotFoundException.class, () -> { + stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); + connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + }); + } + + @Nested + class UserRefusedInteractions { + + @Test + void getSessionStatus_userHasRefused() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessage() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessage", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessageAndVerificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedDisplayTextAndPin() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("displayTextAndPIN", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("verificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } + } + + @Test + void getSessionStatus_userHasRefusedCertChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_CERT_CHOICE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_timeout() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("TIMEOUT", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_userHasSelectedWrongVcCode() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("WRONG_VC", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_documentUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("DOCUMENT_UNUSABLE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_protocolFailure() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-protocol-failure.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PROTOCOL_FAILURE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_expectedLinkedSession() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-expected-linked-session.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("EXPECTED_LINKED_SESSION", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_serverError() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-server-error.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_accountUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-account-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("ACCOUNT_UNUSABLE", sessionStatus.getResult().getEndResult()); + } + + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); + return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + } + + private static void assertSuccessfulResponse(SessionStatus sessionStatus) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertNotNull(sessionStatus.getResult()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); + } + + private static void assertSignatureAlgorithmParameters(SessionSignature sessionSignature, String expectedHashAlgorithm) { + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + assertEquals(expectedHashAlgorithm, signatureAlgorithmParameters.getHashAlgorithm()); + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); + SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); + assertEquals(expectedHashAlgorithm, parameters.getHashAlgorithm()); + assertEquals(64, signatureAlgorithmParameters.getSaltLength()); + assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); + } + } + + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierDeviceLinkAuthentication { + + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-30303039914"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toQrAuthenticationSessionRequest(); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_sameDeviceOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(null, "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_sameDeviceAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(new RequestProperties(true), "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberDeviceLinkAuthentication { + + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initDeviceLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), "PNOEE-48010010101-MOCK-Q")); + } + + @Test + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + } + + @Nested + @WireMockTest(httpPort = 18081) + class AnonymousDeviceLinkAuthentication { + + private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/device-link/anonymous"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18081"); + } + + @Test + void initAnonymousDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initAnonymousDeviceLinkAuthentication_sameDeviceFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initAnonymousDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + } + + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierNotificationAuthentication { + + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initNotificationAuthentication_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), SEMANTICS_IDENTIFIER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberNotificationAuthentication { + + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initNotificationAuthentication_onlyRequeriedFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), DOCUMENT_NUMBER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(authenticationRequest, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkCertificateChoiceTests { + + private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/signature/certificate-choice/device-link/anonymous"; + + private SmartIdConnector connector; + + @BeforeEach + public void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initDeviceLinkCertificateChoice() { + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); + Instant end = Instant.now(); + + assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); + } + + @Test + void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_userAccountNotFound() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_invalidRequest() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); + + var request = new DeviceLinkCertificateChoiceSessionRequest("", "", null, null, null, null, null); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); + } + + @Test + void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); + } + + @Test + void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + private static DeviceLinkCertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + "ADVANCED", + null, + null, + null, + null + ); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationSignature { + + private static final String LINKED_SIGNATURE_PATH = "/signature/notification/linked/PNOEE-31111111111-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111-MOCK-Q"; + private static final String NONCE = "cmFuZG9tTm9uY2U="; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionRequest request = toLinkedSignatureSessionRequest(null, null, null); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = connector.initLinkedNotificationSignature(request, DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_withAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse linkedSignatureSessionResponse = + connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + var linkedSignatureSessionRequest = toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, "cmFuZG9tTm9uY2U=", new RequestProperties(true)); + connector.initLinkedNotificationSignature(linkedSignatureSessionRequest, DOCUMENT_NUMBER); + }); + } + + @Test + void initLinkedNotificationSignature_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + private LinkedSignatureSessionRequest toFullLinkedSignatureSessionRequest() { + return toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, NONCE, new RequestProperties(true)); + } + + private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(CertificateLevel certificateLevel, + String nonce, + RequestProperties requestProperties) { + var rawDigestSignatureProtocolParameters = new RawDigestSignatureProtocolParameters( + "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); + return new LinkedSignatureSessionRequest("00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel != null ? certificateLevel.name() : null, + "RAW_DIGEST_SIGNATURE", + rawDigestSignatureProtocolParameters, + "10000000-0000-000-000-000000000000", + nonce, + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + requestProperties, + null); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class SemanticsIdentifierNotificationCertificateChoiceTests { + + private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/signature/certificate-choice/notification/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initCertificateChoice_onlyRequiredFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @Test + void initCertificateChoice_allFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + var request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + "QUALIFIED", + "cmFuZG9tTm9uY2U=", + null, + new RequestProperties(true)); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @Test + void initCertificateChoice_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_rpDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 471); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_userShouldCheckPortal_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 472); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_javaClientBeingUsedIsTooOld_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 480); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 580); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18086) + class CertificateByDocumentNumberTests { + + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18086"); + } + + @Test + void getCertificateByDocumentNumber_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); + + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest()); + + assertNotNull(response); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); + + var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", null); + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); + + assertNotNull(response); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + } + + @Test + void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); + assertThrows(UserAccountNotFoundException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); + } + + @Test + void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkSignatureTests { + + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initDeviceLinkSignature_withSemanticsIdentifier_successful() { + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); + + assertNotNull(response); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); + } + + @Test + void initDeviceLinkSignature_withDocumentNumber_successful() { + stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + String documentNumber = "PNOEE-31111111111-MOCK-Q"; + + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); + + assertNotNull(response); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); + } + + @Test + void initDeviceLinkSignature_userAccountNotFound() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_relyingPartyNoPermission() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_invalidRequest() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); + } + + @Test + void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsSmartIdClientException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); + } + + @Test + void initDeviceLinkSignature_throwsServerMaintenanceException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18084) + class SemanticsIdentifierNotificationSignature { + + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18084"); + } + + @Test + void initNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-all-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("DEMO", CertificateLevel.QSCD, "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", true); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { + connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + }); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18085) + class DocumentNumberNotificationSignature { + + private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18085"); + } + + @Test + void initNotificationSignature_onlyRequiredFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_allFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("NOT DEMO", null, null, null); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + } + + private DeviceLinkAuthenticationSessionRequest toQrAuthenticationSessionRequest() { + return toDeviceLinkAuthenticationSessionRequest(null, null); + } + + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest(RequestProperties requestProperties, + String initialCallbackUrl) { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + return new DeviceLinkAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + CertificateLevel.QUALIFIED.name(), + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + requestProperties, + null, + initialCallbackUrl + ); + } + + private static NotificationAuthenticationSessionRequest toNotificationAuthenticationSessionRequest(CertificateLevel certificateLevel, RequestProperties requestProperties) { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2.name(), + signatureProtocolParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Login?"))), + requestProperties, + null, + "numeric4" + ); + } + + private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { + return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); + } + + private static DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { + var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA3-512")); + + return new DeviceLinkSignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", + "BANK123", + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign the document", null))), + null, + null); + } + + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest() { + return toNotificationSignatureSessionRequest("DEMO", null, null, null); + } + + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest(String relyingPartyName, + CertificateLevel certificateLevel, + String nonce, + Boolean shareIpAddress) { + var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA-512")); + var interaction = new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Sign it!"); + return new NotificationSignatureSessionRequest("00000000-0000-4000-8000-000000000000", + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + nonce, + null, + InteractionUtil.encodeToBase64(List.of(interaction)), + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null); + } + + private static void assertResponseValues(DeviceLinkSessionResponse response, + String expectedSessionToken, + String expectedSessionSecret, + Instant start, + Instant end) { + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + assertEquals(expectedSessionToken, response.sessionToken()); + assertEquals(expectedSessionSecret, response.sessionSecret()); + assertNotNull(response.receivedAt()); + assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); + assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response); + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); + } +} diff --git a/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java index fd1b36f9..fbf3810a 100644 --- a/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java @@ -1,105 +1,105 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.exception.SessionSecretMismatchException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class CallbackUrlUtilTest { - - private static final String SESSION_SECRET_DIGEST = "nKMc7gT3mvWuJtfXVFjCY2ehuvTs26f1Sgjk6g9oOr8"; - - @Nested - class CreateCallbackUrl { - - @Test - void createCallbackUrl_valueQueryParameterIsSameAsUrlToken() { - CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); - - assertEquals("https://example.com/callback?value=" + callbackUrl.urlToken(), - callbackUrl.initialCallbackUri().toString()); - } - - @ParameterizedTest - @NullAndEmptySource - void createCallbackUrl_inputBaseUrlIsEmpty_throwException(String baseUrl) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.createCallbackUrl(baseUrl)); - assertEquals("Parameter for 'baseUrl' cannot be empty", ex.getMessage()); - } - } - - @Nested - class ValidateSessionSecretDigest { - - @Test - void validateSessionSecretDigest() { - String sessionSecret = "fBo1/L1vM9xcSmZF7hvvooEj"; - assertDoesNotThrow(() -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - } - - @ParameterizedTest - @NullAndEmptySource - void validateSessionSecretDigest_sessionSecretDigestIsEmpty_throwException(String sessionSecretDigest) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(sessionSecretDigest, "")); - assertEquals("Parameter for 'sessionSecretDigest' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateSessionSecretDigest_sessionSecretIsEmpty_throwException(String sessionSecret) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - assertEquals("Parameter for 'sessionSecret' cannot be empty", ex.getMessage()); - } - - @Test - void validateSessionSecretDigest_sessionSecretValidationFails_throwException() { - String sessionSecret = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); - - var ex = assertThrows(SessionSecretMismatchException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - assertEquals("Session secret digest from callback does not match calculated session secret digest", ex.getMessage()); - } - - @Test - void validateSessionSecretDigest_sessionSecretIsNotBase64Encoded_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, "sessionSecret")); - assertEquals("Parameter 'sessionSecret' is not Base64-encoded value", ex.getMessage()); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class CallbackUrlUtilTest { + + private static final String SESSION_SECRET_DIGEST = "nKMc7gT3mvWuJtfXVFjCY2ehuvTs26f1Sgjk6g9oOr8"; + + @Nested + class CreateCallbackUrl { + + @Test + void createCallbackUrl_valueQueryParameterIsSameAsUrlToken() { + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + + assertEquals("https://example.com/callback?value=" + callbackUrl.urlToken(), + callbackUrl.initialCallbackUri().toString()); + } + + @ParameterizedTest + @NullAndEmptySource + void createCallbackUrl_inputBaseUrlIsEmpty_throwException(String baseUrl) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.createCallbackUrl(baseUrl)); + assertEquals("Parameter for 'baseUrl' cannot be empty", ex.getMessage()); + } + } + + @Nested + class ValidateSessionSecretDigest { + + @Test + void validateSessionSecretDigest() { + String sessionSecret = "fBo1/L1vM9xcSmZF7hvvooEj"; + assertDoesNotThrow(() -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretDigestIsEmpty_throwException(String sessionSecretDigest) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(sessionSecretDigest, "")); + assertEquals("Parameter for 'sessionSecretDigest' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretIsEmpty_throwException(String sessionSecret) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Parameter for 'sessionSecret' cannot be empty", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretValidationFails_throwException() { + String sessionSecret = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + + var ex = assertThrows(SessionSecretMismatchException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Session secret digest from callback does not match calculated session secret digest", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretIsNotBase64Encoded_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, "sessionSecret")); + assertEquals("Parameter 'sessionSecret' is not Base64-encoded value", ex.getMessage()); + } + } +} diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index 3e742f4e..aca5b143 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -1,142 +1,142 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.InvalidCertificateGenerator; - -public class CertificateAttributeUtilTest { - - private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; - private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; - - @Test - public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { - X509Certificate certificateWithDob = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - - LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); - - assertThat(dateOfBirthCertificateAttribute, is(notNullValue())); - assertThat(dateOfBirthCertificateAttribute, is(LocalDate.of(1903, 3, 3))); - } - - @Test - public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { - X509Certificate certificateWithoutDobAttribute = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); - - LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); - - assertThat(dateOfBirthCertificateAttribute, is(nullValue())); - } - - @ParameterizedTest - @ArgumentsSource(AttributeArgumentProvider.class) - void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - - Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); - - assertTrue(attributeValue.isPresent()); - assertThat(attributeValue.get(), is(expectedValue)); - } - - @Test - void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - - Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); - - assertTrue(attributeValue.isEmpty()); - } - - @Test - void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); - - assertTrue(certificatePolicy.isEmpty()); - } - - @Test - void getCertificatePolicy_certificatePolicyPresent() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); - - Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); - - assertThat(certificatePolicy, contains("1.3.6.1.4.1.10015.3.17.2", "0.4.0.2042.1.1")); - } - - @Test - void hasNonRepudiation_KeyUsageExtensionIsMissing() { - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withKeyUsage(null) - .createCertificate(); - - assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); - } - - private static class AttributeArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Given name", BCStyle.GIVENNAME), "BOD"), - Arguments.of(Named.of("Surname", BCStyle.SURNAME), "TESTNUMBER"), - Arguments.of(Named.of("Serial number", BCStyle.SERIALNUMBER), "PNOLV-329999-99901"), - Arguments.of(Named.of("Country", BCStyle.C), "LV") - ); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.InvalidCertificateGenerator; + +public class CertificateAttributeUtilTest { + + private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; + private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; + + @Test + public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { + X509Certificate certificateWithDob = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + + LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); + + assertThat(dateOfBirthCertificateAttribute, is(notNullValue())); + assertThat(dateOfBirthCertificateAttribute, is(LocalDate.of(1903, 3, 3))); + } + + @Test + public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { + X509Certificate certificateWithoutDobAttribute = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); + + LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); + + assertThat(dateOfBirthCertificateAttribute, is(nullValue())); + } + + @ParameterizedTest + @ArgumentsSource(AttributeArgumentProvider.class) + void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); + + assertTrue(attributeValue.isPresent()); + assertThat(attributeValue.get(), is(expectedValue)); + } + + @Test + void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); + + assertTrue(attributeValue.isEmpty()); + } + + @Test + void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertTrue(certificatePolicy.isEmpty()); + } + + @Test + void getCertificatePolicy_certificatePolicyPresent() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertThat(certificatePolicy, contains("1.3.6.1.4.1.10015.3.17.2", "0.4.0.2042.1.1")); + } + + @Test + void hasNonRepudiation_KeyUsageExtensionIsMissing() { + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withKeyUsage(null) + .createCertificate(); + + assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); + } + + private static class AttributeArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Given name", BCStyle.GIVENNAME), "BOD"), + Arguments.of(Named.of("Surname", BCStyle.SURNAME), "TESTNUMBER"), + Arguments.of(Named.of("Serial number", BCStyle.SERIALNUMBER), "PNOLV-329999-99901"), + Arguments.of(Named.of("Country", BCStyle.C), "LV") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java index 47e53d1f..6c174734 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java @@ -1,141 +1,141 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationIdentityMapper; -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -public class NationalIdentityNumberUtilTest { - - private static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; - private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; - private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; - - @Test - public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { - X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { - X509Certificate lvCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1903, 4, 3))); - } - - @Test - public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { - X509Certificate ltCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1960, 9, 6))); - } - - @ParameterizedTest - @ValueSource(strings = {"321205-1234", "331205-1234", "341205-1234", "351205-1234", "361205-1234", "371205-1234", "381205-1234", "391205-1234"}) - public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull(String lvNationalIdentityNumber) { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth(lvNationalIdentityNumber); - assertThat(birthDate, is(nullValue())); - } - - @Test - public void parseLvDateOfBirth_21century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131205-2234"); - assertThat(birthDate, is(LocalDate.of(2005, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_20century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-1234"); - assertThat(birthDate, is(LocalDate.of(1965, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_19century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-0234"); - assertThat(birthDate, is(LocalDate.of(1865, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_invalidMonth_throwsException() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, - () -> NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234")); - - assertThat(unprocessableSmartIdResponseException.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); - } - - @Test - public void getDateOfBirthFromIdCode_sweden_returnsNull() { - AuthenticationIdentity identity = new AuthenticationIdentity(); - identity.setCountry("SE"); - identity.setIdentityNumber("1995012-79039"); - - assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); - } - - @Test - public void getDateOfBirthFromIdCode_poland_returnsNull() { - AuthenticationIdentity identity = new AuthenticationIdentity(); - identity.setCountry("PL"); - identity.setIdentityNumber("64120301283"); - - assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); - } - -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentityMapper; +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public class NationalIdentityNumberUtilTest { + + private static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; + private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; + private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; + + @Test + public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { + X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { + X509Certificate lvCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1903, 4, 3))); + } + + @Test + public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { + X509Certificate ltCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1960, 9, 6))); + } + + @ParameterizedTest + @ValueSource(strings = {"321205-1234", "331205-1234", "341205-1234", "351205-1234", "361205-1234", "371205-1234", "381205-1234", "391205-1234"}) + public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull(String lvNationalIdentityNumber) { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth(lvNationalIdentityNumber); + assertThat(birthDate, is(nullValue())); + } + + @Test + public void parseLvDateOfBirth_21century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131205-2234"); + assertThat(birthDate, is(LocalDate.of(2005, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_20century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-1234"); + assertThat(birthDate, is(LocalDate.of(1965, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_19century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-0234"); + assertThat(birthDate, is(LocalDate.of(1865, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_invalidMonth_throwsException() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, + () -> NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234")); + + assertThat(unprocessableSmartIdResponseException.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); + } + + @Test + public void getDateOfBirthFromIdCode_sweden_returnsNull() { + AuthenticationIdentity identity = new AuthenticationIdentity(); + identity.setCountry("SE"); + identity.setIdentityNumber("1995012-79039"); + + assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); + } + + @Test + public void getDateOfBirthFromIdCode_poland_returnsNull() { + AuthenticationIdentity identity = new AuthenticationIdentity(); + identity.setCountry("PL"); + identity.setIdentityNumber("64120301283"); + + assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); + } + +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index c05b1e63..34e98e4b 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -1,15 +1,15 @@ - - - - - %d{dd.MM.yyyy HH:mm:ss.SSS} %-5p [%thread] [%logger{36}.%method:%line] - %m%n - - - - - - - - - - + + + + + %d{dd.MM.yyyy HH:mm:ss.SSS} %-5p [%thread] [%logger{36}.%method:%line] - %m%n + + + + + + + + + + diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json index 58621ea6..fa1ecb06 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json @@ -1,7 +1,7 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json index 0ccbe990..29a54242 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json index 2f6ed5ce..9d5e2ad7 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "certificateLevel": "QUALIFIED", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", - "requestProperties": { - "shareMdClientIpAddress": true - }, - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json index 4a0177cb..9712dd01 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json @@ -1,15 +1,15 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json index 683dbb84..8adf3305 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", - "requestProperties": { - "shareMdClientIpAddress": true - }, - "vcType": "numeric4" -} +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "vcType": "numeric4" +} diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json index 22276dab..a9dbf465 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" -} +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" +} diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json index 5fd36502..7efc8494 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", - "vcType": "numeric4" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "vcType": "numeric4" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json index f1d018f4..ee280a89 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json @@ -1,5 +1,5 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "ADVANCED" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json index 39c99d9e..b006f4df 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json index e19b9623..9653b796 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json index b980efff..5732ba95 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json index 00ceb1cb..5caee0c0 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json index 16cf47ee..3da1f40c 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json @@ -1,10 +1,10 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "initialCallbackUrl": "https://example.com/callback", - "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback", + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json index 4c8d933b..b06bd4e5 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json @@ -1,6 +1,6 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json index f1d018f4..ee280a89 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json @@ -1,5 +1,5 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "ADVANCED" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json index 7c17de75..c233c758 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json @@ -1,19 +1,19 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "linkedSessionID": "10000000-0000-000-000-000000000000", - "nonce": "cmFuZG9tTm9uY2U=", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "nonce": "cmFuZG9tTm9uY2U=", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json index 0b2cc34d..7fcdc646 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "linkedSessionID": "10000000-0000-000-000-000000000000", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json index f44f8d16..7862df88 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json @@ -1,9 +1,9 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "nonce": "cmFuZG9tTm9uY2U=", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "nonce": "cmFuZG9tTm9uY2U=", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json index 5b1cac06..8ed2cc2f 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "NOT DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json index c9b762c0..92c7a7ef 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json @@ -1,3 +1,3 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json index 39c99d9e..b006f4df 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json index fea28091..6028bfe7 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QSCD", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QSCD", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json index 5a0ac32e..272ed234 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "NOT DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json index c9b762c0..92c7a7ef 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json @@ -1,3 +1,3 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json index 5d0e25f1..0f62fd1b 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" } \ No newline at end of file diff --git a/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json index 79fb9fdb..d4cc540f 100644 --- a/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json +++ b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "sessionToken": "sessionToken", - "sessionSecret": "c2Vzc2lvblNlY3JldA==", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "sessionToken": "sessionToken", + "sessionSecret": "c2Vzc2lvblNlY3JldA==", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/auth/notification/notification-session-response.json b/src/test/resources/responses/auth/notification/notification-session-response.json index 6b21efb5..836fc071 100644 --- a/src/test/resources/responses/auth/notification/notification-session-response.json +++ b/src/test/resources/responses/auth/notification/notification-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json index 5491eb3e..53c69870 100644 --- a/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json +++ b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json @@ -1,9 +1,9 @@ -{ - "state": "UNKNOWN", - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "UNKNOWN", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/certificate-by-document-number-response.json b/src/test/resources/responses/certificate-by-document-number-response.json index 78e18794..49886071 100644 --- a/src/test/resources/responses/certificate-by-document-number-response.json +++ b/src/test/resources/responses/certificate-by-document-number-response.json @@ -1,9 +1,9 @@ -{ - "state": "OK", - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "OK", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-account-unusable.json b/src/test/resources/responses/session-status-account-unusable.json index 9cf51132..247caa53 100644 --- a/src/test/resources/responses/session-status-account-unusable.json +++ b/src/test/resources/responses/session-status-account-unusable.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "ACCOUNT_UNUSABLE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "ACCOUNT_UNUSABLE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-document-unusable.json b/src/test/resources/responses/session-status-document-unusable.json index 82f93325..69d2dfa8 100644 --- a/src/test/resources/responses/session-status-document-unusable.json +++ b/src/test/resources/responses/session-status-document-unusable.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "DOCUMENT_UNUSABLE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "DOCUMENT_UNUSABLE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-expected-linked-session.json b/src/test/resources/responses/session-status-expected-linked-session.json index ea5b572f..66c88f0e 100644 --- a/src/test/resources/responses/session-status-expected-linked-session.json +++ b/src/test/resources/responses/session-status-expected-linked-session.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "EXPECTED_LINKED_SESSION", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "EXPECTED_LINKED_SESSION", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-protocol-failure.json b/src/test/resources/responses/session-status-protocol-failure.json index 77efaa4d..c3c4c5d0 100644 --- a/src/test/resources/responses/session-status-protocol-failure.json +++ b/src/test/resources/responses/session-status-protocol-failure.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "PROTOCOL_FAILURE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "PROTOCOL_FAILURE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-running-with-ignored-properties.json b/src/test/resources/responses/session-status-running-with-ignored-properties.json index b44c1615..a9e2800f 100644 --- a/src/test/resources/responses/session-status-running-with-ignored-properties.json +++ b/src/test/resources/responses/session-status-running-with-ignored-properties.json @@ -1,5 +1,5 @@ -{ - "state": "RUNNING", - "ignoredProperties": ["testingIgnored", "testingIgnoredTwo"], - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "RUNNING", + "ignoredProperties": ["testingIgnored", "testingIgnoredTwo"], + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-running.json b/src/test/resources/responses/session-status-running.json index 221b89f7..b7ec24cb 100644 --- a/src/test/resources/responses/session-status-running.json +++ b/src/test/resources/responses/session-status-running.json @@ -1,4 +1,4 @@ -{ - "state": "RUNNING", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "RUNNING", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-server-error.json b/src/test/resources/responses/session-status-server-error.json index d9e2716a..a914c4f5 100644 --- a/src/test/resources/responses/session-status-server-error.json +++ b/src/test/resources/responses/session-status-server-error.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "SERVER_ERROR", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "SERVER_ERROR", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-authentication.json b/src/test/resources/responses/session-status-successful-authentication.json index bdda60ba..62fdd536 100644 --- a/src/test/resources/responses/session-status-successful-authentication.json +++ b/src/test/resources/responses/session-status-successful-authentication.json @@ -1,38 +1,38 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "signatureProtocol": "ACSP_V2", - "signature": { - "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", - "serverRandom": "J0iyCYOu8cTWuoD8rD05IIrZ", - "userChallenge": "GnsWXXEjTCKR89fj9uo5u5ReBZ9JR7_pezLAI5jMS00", - "flowType": "QR", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512", - "maskGenAlgorithm": { - "algorithm": "id-mgf1", - "parameters": { - "hashAlgorithm": "SHA3-512", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "saltLength": 64, - "trailerField": "0xbc", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQybEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7CXahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEOTnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5nzCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcyhPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29JcowTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9PsmN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1JgxBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRjFisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QACBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v09hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSgAt+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0GCysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rlc3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHKmTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0XnPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxWCbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2", - "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "interactionTypeUsed": "displayTextAndPIN", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "signatureProtocol": "ACSP_V2", + "signature": { + "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", + "serverRandom": "J0iyCYOu8cTWuoD8rD05IIrZ", + "userChallenge": "GnsWXXEjTCKR89fj9uo5u5ReBZ9JR7_pezLAI5jMS00", + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA3-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQybEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7CXahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEOTnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5nzCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcyhPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29JcowTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9PsmN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1JgxBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRjFisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QACBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v09hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSgAt+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0GCysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rlc3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHKmTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0XnPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxWCbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2", + "certificateLevel": "QUALIFIED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "interactionTypeUsed": "displayTextAndPIN", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-certificate-choice.json b/src/test/resources/responses/session-status-successful-certificate-choice.json index df74f5e2..70040c89 100644 --- a/src/test/resources/responses/session-status-successful-certificate-choice.json +++ b/src/test/resources/responses/session-status-successful-certificate-choice.json @@ -1,14 +1,14 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json index f5372743..4de6f326 100644 --- a/src/test/resources/responses/session-status-successful-signature.json +++ b/src/test/resources/responses/session-status-successful-signature.json @@ -1,37 +1,37 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signature": { - "value": "fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgLLScB4+0qEhji9HKNRNHpXsip6LmoDiWD7pBlBPL0YOsFczSEpRpCe3NLxWWCWzd7i6tFcYXFwhpXEaUtoUhpstGOtjYGHkvXzMcQmiyXC4qWrw3RrSqEnB2ONmuZE60brwyRne8xYgBMmHcvu0s9jcTDWM+ppNfjm4WED+u5sOTGbSyO7Eg9kOhfDenYg++1Cg6zlpWwd9OMpojmK2pOsZC0JmcOIyQ+Cf2mBobx0qt6cPot9/bx1X5uTualJfxMrRZSE3twuXq0f3f0A+Yv3kHhx/AdzQaAuydtIdlz60naWIS84PUnAeOKiYLRbRRawLc4MGZHqn4DeFHI4zvzMLhz13O8pirFWb7qWJ+RvsgyAMTHmAwzPmtpwYT90z22Bc915qTufaJ48/m8DXGARQdbOP+/+5a4Q7PwnrdAm7SwbnNcAlvzVQO+o1onhnPKGz79EYVIgNj+9Hijqdggw41lBEjl82Lr7LNuVhz2wVaBYD4yELzmoDEOW69wWMQ6WHwK/SF1Xe44ENi6JSZE1f19AQT0+xOt0FWKloQ9Tn/kvtw+/LhLzugOtf61t9HBLCt73iSpJ6SqD4lMHxozJ5SEJNm05DBhaCf3IlZzw0HYFRMZNUXx/7y2QhOWpRMFZIhjHjFedi1IxPj3BmKTL1Vgq5koCDxF1Wbdl+UONK9UthYpKpU13Wi04YubYLb3VKw9wb9f9YlweXoeUHxOTy3l6f+Z6lP3EYAp7NbyJlPCW7yhTeS4kg4uzftqr+2cW4ORdQvs2Va7qrkdu5sd8d72jKWuQluviR5gCTLvQtttc/Tex/ix8iuQ4ffHTap+gnrcEgIA3Th8Z0m93kwpE+YLjHAxMQmzgkR/iPoDpTutpqjoLbrhLgUQpSJ5pYyRQgc6iM/BN6+xpe2GFBoODXzBj81OK1qDN89A26ldyLDan0tkSKIuVJWIapDxQick", - "flowType": "QR", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512", - "maskGenAlgorithm": { - "algorithm": "id-mgf1", - "parameters": { - "hashAlgorithm": "SHA-512", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "saltLength": 64, - "trailerField": "0xbc", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "interactionTypeUsed": "verificationCodeChoice", - "deviceIpAddress": "203.0.113.34", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signature": { + "value": "fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgLLScB4+0qEhji9HKNRNHpXsip6LmoDiWD7pBlBPL0YOsFczSEpRpCe3NLxWWCWzd7i6tFcYXFwhpXEaUtoUhpstGOtjYGHkvXzMcQmiyXC4qWrw3RrSqEnB2ONmuZE60brwyRne8xYgBMmHcvu0s9jcTDWM+ppNfjm4WED+u5sOTGbSyO7Eg9kOhfDenYg++1Cg6zlpWwd9OMpojmK2pOsZC0JmcOIyQ+Cf2mBobx0qt6cPot9/bx1X5uTualJfxMrRZSE3twuXq0f3f0A+Yv3kHhx/AdzQaAuydtIdlz60naWIS84PUnAeOKiYLRbRRawLc4MGZHqn4DeFHI4zvzMLhz13O8pirFWb7qWJ+RvsgyAMTHmAwzPmtpwYT90z22Bc915qTufaJ48/m8DXGARQdbOP+/+5a4Q7PwnrdAm7SwbnNcAlvzVQO+o1onhnPKGz79EYVIgNj+9Hijqdggw41lBEjl82Lr7LNuVhz2wVaBYD4yELzmoDEOW69wWMQ6WHwK/SF1Xe44ENi6JSZE1f19AQT0+xOt0FWKloQ9Tn/kvtw+/LhLzugOtf61t9HBLCt73iSpJ6SqD4lMHxozJ5SEJNm05DBhaCf3IlZzw0HYFRMZNUXx/7y2QhOWpRMFZIhjHjFedi1IxPj3BmKTL1Vgq5koCDxF1Wbdl+UONK9UthYpKpU13Wi04YubYLb3VKw9wb9f9YlweXoeUHxOTy3l6f+Z6lP3EYAp7NbyJlPCW7yhTeS4kg4uzftqr+2cW4ORdQvs2Va7qrkdu5sd8d72jKWuQluviR5gCTLvQtttc/Tex/ix8iuQ4ffHTap+gnrcEgIA3Th8Z0m93kwpE+YLjHAxMQmzgkR/iPoDpTutpqjoLbrhLgUQpSJ5pYyRQgc6iM/BN6+xpe2GFBoODXzBj81OK1qDN89A26ldyLDan0tkSKIuVJWIapDxQick", + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "interactionTypeUsed": "verificationCodeChoice", + "deviceIpAddress": "203.0.113.34", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/session-status-timeout.json b/src/test/resources/responses/session-status-timeout.json index 32dae0d6..8dfe0a10 100644 --- a/src/test/resources/responses/session-status-timeout.json +++ b/src/test/resources/responses/session-status-timeout.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "TIMEOUT", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "TIMEOUT", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-cert-choice.json b/src/test/resources/responses/session-status-user-refused-cert-choice.json index bd4616a7..c89ba60e 100644 --- a/src/test/resources/responses/session-status-user-refused-cert-choice.json +++ b/src/test/resources/responses/session-status-user-refused-cert-choice.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_CERT_CHOICE", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_CERT_CHOICE", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json index 62656ae6..24ce4f44 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "confirmationMessageAndVerificationCodeChoice", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessageAndVerificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-confirmation.json b/src/test/resources/responses/session-status-user-refused-confirmation.json index d3b1af6e..9c9507ff 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "confirmationMessage", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessage", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json index be9fecf6..9817103e 100644 --- a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json +++ b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "displayTextAndPIN", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "displayTextAndPIN", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-vc-choice.json b/src/test/resources/responses/session-status-user-refused-vc-choice.json index a06e5f09..038c9eef 100644 --- a/src/test/resources/responses/session-status-user-refused-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-vc-choice.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "verificationCodeChoice", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "verificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused.json b/src/test/resources/responses/session-status-user-refused.json index 7c3e55eb..1b3d0346 100644 --- a/src/test/resources/responses/session-status-user-refused.json +++ b/src/test/resources/responses/session-status-user-refused.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-wrong-vc.json b/src/test/resources/responses/session-status-wrong-vc.json index 9dd59fc9..b32bef4c 100644 --- a/src/test/resources/responses/session-status-wrong-vc.json +++ b/src/test/resources/responses/session-status-wrong-vc.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "WRONG_VC", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "WRONG_VC", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json index ae3b7df6..2305654b 100644 --- a/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json +++ b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "test-session-id", - "sessionToken": "test-session-token", - "sessionSecret": "c2Vzc2lvblNlY3JldA==", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "test-session-id", + "sessionToken": "test-session-token", + "sessionSecret": "c2Vzc2lvblNlY3JldA==", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json index 3e18ae14..739a88d1 100644 --- a/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json +++ b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "sessionToken": "sampleSessionToken", - "sessionSecret": "sampleSessionSecret", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "sessionToken": "sampleSessionToken", + "sessionSecret": "sampleSessionSecret", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json index 29b7df71..6186cf1b 100644 --- a/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json +++ b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json index 29b7df71..6186cf1b 100644 --- a/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json +++ b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json index 7ba7d5d3..1133b2a4 100644 --- a/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json +++ b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "vc": { - "type": "numeric4", - "value": "0000" - } +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "vc": { + "type": "numeric4", + "value": "0000" + } } \ No newline at end of file diff --git a/src/test/resources/sid_demo_sk_ee.pem b/src/test/resources/sid_demo_sk_ee.pem index 7ae6d9c8..0dcd5561 100644 --- a/src/test/resources/sid_demo_sk_ee.pem +++ b/src/test/resources/sid_demo_sk_ee.pem @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGxTCCBa2gAwIBAgIQB//0m9ljohCn8LB5KDcE1jANBgkqhkiG9w0BAQsFADBZ -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE -aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQx -MDAzMDAwMDAwWhcNMjUxMDE0MjM1OTU5WjBVMQswCQYDVQQGEwJFRTEQMA4GA1UE -BxMHVGFsbGlubjEbMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQQD -Ew5zaWQuZGVtby5zay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKAyy0yvjRCrATznThIwCu/wPCU5mV5UZIzNWl9KXx+gQiBp92SXfTOokkfiikBH -09HI+yVr3zI2U6FR8Tj21GiFE3bttmpCw8tJLmTe/P0Xah1D6vVkymbBt69N24ur -RqhW9in84WdkPc30vGJ+TdIj3jIePAbK3hHbpm+BfeyUhM48xXRgW+cBA//6R1C9 -lUaF9Ycylf+g/P7FpmzHRk2HF3bPyWziBVOhIADtqMyVEJk20dl0SWGsCmAJuAhM -mOPc87zpXYzlAlY24XgsTyQdDnqmJn8ZukDahIt9ybKH/WPLkZfw6xBnsQKXdG0J -HBqBsgQdPDFsrsY45o4ek0kCAwEAAaOCA4swggOHMB8GA1UdIwQYMBaAFHSFgMBm -x9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBSK7cmy40mto6zFVpcvnOyggb6YnzAZ -BgNVHREEEjAQgg5zaWQuZGVtby5zay5lZTA+BgNVHSAENzA1MDMGBmeBDAECAjAp -MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0P -AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0f -BIGXMIGUMEigRqBEhkJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH -bG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9j -cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAy -MENBMS0xLmNybDCBhwYIKwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8v -b2NzcC5kaWdpY2VydC5jb20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRp -Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0x -LmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdwAS -8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZJR+i+zAAAEAwBIMEYC -IQC7tPwb72Mur1ljtCP8g1/BkS6nJV0QeueW3eSa2L+PkwIhAPCJOyx++Vg5mE5D -6S0ctqbVRQsM5XGKYrBzAyzh0QHaAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65 -Ay/ZDowuebgAAAGSUfovdQAABAMARzBFAiEA6ifcmc/Si0vOqT4JTAMqervuE7Uz -iYGZIIZI09BYINICICeJuQZrqP7aHqn9+0iyvl5ptJl2cZ5YyqF3Km9f6vu4AHYA -5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6hzId/R43jlAAAAGSUfovjAAABAMARzBF -AiEAkdK3dAY6ABFtaE1bTjIlYAF5cFT8N2pvxL0mA79LlDwCIFGZJ3EYJfxVbj9m -S/8FynieG/02iMF6xzmmrU58La0pMA0GCSqGSIb3DQEBCwUAA4IBAQCnq3OnD4uw -uvt75qYIBgFNN+nIMslacl8iQYSOswr+K90QzL/yf+lLafDX0QMtDL5b2t1a834R -8efjlEuISfp+YjTdtnNV1jZ7nnkHcFMP1MGbv/JQigPO8AgL+oxGHiRCp6FNJTwt -FtvHkqd5rDJUU988LdND4aYtmKYmGKj06sSqhpl9xmbIxdXPvaJGoHC/gEpM8AKw -oL4afke2q3FpjQ1eDT+37pjsEjQi6nT0/cSNoyxy4QbqWBgGclmb9ZAfOFkaO5U3 -bhRopdPzRSrQROUF0ovPk4aC+b74KAV/oxtQjPTdpdxTVBwjfn2tpes5q+TZUGSZ -AyP23gCAvmuj ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGxTCCBa2gAwIBAgIQB//0m9ljohCn8LB5KDcE1jANBgkqhkiG9w0BAQsFADBZ +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE +aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQx +MDAzMDAwMDAwWhcNMjUxMDE0MjM1OTU5WjBVMQswCQYDVQQGEwJFRTEQMA4GA1UE +BxMHVGFsbGlubjEbMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQQD +Ew5zaWQuZGVtby5zay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKAyy0yvjRCrATznThIwCu/wPCU5mV5UZIzNWl9KXx+gQiBp92SXfTOokkfiikBH +09HI+yVr3zI2U6FR8Tj21GiFE3bttmpCw8tJLmTe/P0Xah1D6vVkymbBt69N24ur +RqhW9in84WdkPc30vGJ+TdIj3jIePAbK3hHbpm+BfeyUhM48xXRgW+cBA//6R1C9 +lUaF9Ycylf+g/P7FpmzHRk2HF3bPyWziBVOhIADtqMyVEJk20dl0SWGsCmAJuAhM +mOPc87zpXYzlAlY24XgsTyQdDnqmJn8ZukDahIt9ybKH/WPLkZfw6xBnsQKXdG0J +HBqBsgQdPDFsrsY45o4ek0kCAwEAAaOCA4swggOHMB8GA1UdIwQYMBaAFHSFgMBm +x9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBSK7cmy40mto6zFVpcvnOyggb6YnzAZ +BgNVHREEEjAQgg5zaWQuZGVtby5zay5lZTA+BgNVHSAENzA1MDMGBmeBDAECAjAp +MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0f +BIGXMIGUMEigRqBEhkJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9j +cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAy +MENBMS0xLmNybDCBhwYIKwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC5kaWdpY2VydC5jb20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRp +Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0x +LmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdwAS +8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZJR+i+zAAAEAwBIMEYC +IQC7tPwb72Mur1ljtCP8g1/BkS6nJV0QeueW3eSa2L+PkwIhAPCJOyx++Vg5mE5D +6S0ctqbVRQsM5XGKYrBzAyzh0QHaAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65 +Ay/ZDowuebgAAAGSUfovdQAABAMARzBFAiEA6ifcmc/Si0vOqT4JTAMqervuE7Uz +iYGZIIZI09BYINICICeJuQZrqP7aHqn9+0iyvl5ptJl2cZ5YyqF3Km9f6vu4AHYA +5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6hzId/R43jlAAAAGSUfovjAAABAMARzBF +AiEAkdK3dAY6ABFtaE1bTjIlYAF5cFT8N2pvxL0mA79LlDwCIFGZJ3EYJfxVbj9m +S/8FynieG/02iMF6xzmmrU58La0pMA0GCSqGSIb3DQEBCwUAA4IBAQCnq3OnD4uw +uvt75qYIBgFNN+nIMslacl8iQYSOswr+K90QzL/yf+lLafDX0QMtDL5b2t1a834R +8efjlEuISfp+YjTdtnNV1jZ7nnkHcFMP1MGbv/JQigPO8AgL+oxGHiRCp6FNJTwt +FtvHkqd5rDJUU988LdND4aYtmKYmGKj06sSqhpl9xmbIxdXPvaJGoHC/gEpM8AKw +oL4afke2q3FpjQ1eDT+37pjsEjQi6nT0/cSNoyxy4QbqWBgGclmb9ZAfOFkaO5U3 +bhRopdPzRSrQROUF0ovPk4aC+b74KAV/oxtQjPTdpdxTVBwjfn2tpes5q+TZUGSZ +AyP23gCAvmuj +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt index 054743a0..b16257c4 100644 --- a/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt +++ b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt @@ -1,17 +1,17 @@ ------BEGIN CERTIFICATE----- -MIICxDCCAiagAwIBAgIQGjWemJjC5ORg6CkyNQ5DzTAKBggqhkjOPQQDBDBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwHhcNMjEwNzA5MTA0NzE0WhcNNDEwNzA5MTA0NzE0WjBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABACGx6ye24WAORL1 -8N0SquoI3TTJ3dd2EcZLs+wZY0XWYzPa0S4o8BKZQTCDbXz9O2x94hpdAjZ4S3Q2 -N7DAvQ0FfAHmM2JotR4UnYvxYv4JxJHpoRvrQoXOXdqO/wMymiPKTXHPFQz6nxxa -ORjy8xsrQeIdrTLj3c+HDVBRA5yE/IXed6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFOIc3mPcvviEfgE7LkuAseF/1fHmMB8G -A1UdIwQYMBaAFOIc3mPcvviEfgE7LkuAseF/1fHmMAoGCCqGSM49BAMEA4GLADCB -hwJBNDZ3R6qmJqL5bQf01oT369DEGcLhr2vA00nRZSqeaaLMfq+RQW8aYl0njfIZ -JAC6q6IJklpH5IyYrcZ29tcBrxECQgFH5aw8ZORororrLDPl1yY2RgsCO1SFoDh5 -eMEaKVtRKNSG1jLzfgiZJOdtIj/h/l/4oDc5DrDDY6kbAnl4M5pDKw== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICxDCCAiagAwIBAgIQGjWemJjC5ORg6CkyNQ5DzTAKBggqhkjOPQQDBDBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzA5MTA0NzE0WhcNNDEwNzA5MTA0NzE0WjBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABACGx6ye24WAORL1 +8N0SquoI3TTJ3dd2EcZLs+wZY0XWYzPa0S4o8BKZQTCDbXz9O2x94hpdAjZ4S3Q2 +N7DAvQ0FfAHmM2JotR4UnYvxYv4JxJHpoRvrQoXOXdqO/wMymiPKTXHPFQz6nxxa +ORjy8xsrQeIdrTLj3c+HDVBRA5yE/IXed6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFOIc3mPcvviEfgE7LkuAseF/1fHmMB8G +A1UdIwQYMBaAFOIc3mPcvviEfgE7LkuAseF/1fHmMAoGCCqGSM49BAMEA4GLADCB +hwJBNDZ3R6qmJqL5bQf01oT369DEGcLhr2vA00nRZSqeaaLMfq+RQW8aYl0njfIZ +JAC6q6IJklpH5IyYrcZ29tcBrxECQgFH5aw8ZORororrLDPl1yY2RgsCO1SFoDh5 +eMEaKVtRKNSG1jLzfgiZJOdtIj/h/l/4oDc5DrDDY6kbAnl4M5pDKw== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer index b9da8a0c..3cf570f6 100644 --- a/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer +++ b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer @@ -1,28 +1,28 @@ ------BEGIN CERTIFICATE----- -MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 -IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN -MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp -Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg -b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr -LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik -gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd -r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs -z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9 -OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb -wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg -RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE -FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D -AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A -aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w -JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME -GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw -czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN -BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR -aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo -MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA -nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24 -mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0 -dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 +IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN +MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp +Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg +b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr +LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik +gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd +r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs +z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9 +OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb +wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg +RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE +FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D +AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A +aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w +JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME +GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw +czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN +BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR +aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo +MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA +nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24 +mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0 +dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt index 74b2a841..cdb18ae6 100644 --- a/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt +++ b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGqDCCBi6gAwIBAgIQDG5nZZQTInaS9mOLZwUmLDAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjUwOTA4MTIyNTIyWhcNMjgwOTA3MTIyNTIxWjBX -MQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzENMAsGA1UEBAwEVEVTVDEL -MAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkq -hkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjZXgGleu94rffU4c2iMM6J0eunUhbISt -ps5unECMwjKRohRJkwJlWToHv99Qs90vFM6rxMmZVKS6MZJ3WjkgbE0dxVVsWLnA -oBZWaZIidAgmQ6l0Cj0bLKjrRzKU/8T4gqy8tVrOJCvvwAlUIawzUdkQ+WdBCu79 -ipm2Lhh0KYkc20a9jNPfFwCOTkuO1AcVkhN6kdjIZgLrtlngKuhqNyotQmJ4Tl7+ -HkAtKV1zojIAY/LusoB5WWivdp6ey7hW4CyFAUhWpRnILcGeQ7sRBswQthEERThA -d5GRuiRWiz6eGrMAo7niINwoDNWwJDCeeAiJtYsjUBVyMyjxbPhX6DtwwZ3gP3NO -iwBQJ3qGDPNu0zGVdbph8+x9NZvkzAcPi8kr67S377BiH8AZPThzzkbadZxgIJrF -9WsTneKzAAk37HfGs3ESpFYaj8jlrI9RGJldh/nhTEdanqLhbGPO0gLXPJXR7pDG -+X9p6lC+sAa6mLVvBE5YSjQJoKCq8fsOfEQ0kyLcNem7rkxLh1ybsk57aDmZPPmx -Su853re+WvqJR8zPgD1qs7/LSKWeOOyi56OcVQ7Dmp6c56JH5JKQdUGbEXtkH+b6 -hHSMTXB/q/1OBwM4Tf7qO+AYU6MDVDdiEC0yAJm85b6cjbaxmY8a8eR2E3QuhV0H -7XLig9ebhoVb8cPJQmJxLMB1UHM4klQcav4F7+aaIOmEx60L0c7IITHU524LVjsP -GyqstXZxugSYh9V0h+aMdhtAj/JhWIh50fYrcaiyXZBu/nyRvzkY/6m5w/ycdiJ4 -tTNn+R5sJzdpo6SHQ5FnKRnpRnS7vrVSOy/quuYvvE06dE4fZrJfxAsGvg1L+qNT -yhpXECq9AF8U4Mqp03fnI5Dxglhx5rmNJZQKFM7PEDul0dV3lyAf6fmDPj/5H4L7 -RQyh5RcPEKbyk3Aw6v6RT69VuxXO2MvWDQx+S96Hk4105+BzWMWbOYyCWDRJz7IP -WTMQW5q1VnZTKElR1kxj6Ae7pITrlDDnAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAA -MB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQw -YjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k -ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIw -MjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1E -RU1PLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0 -cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlv -bi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsG -AQUFBwkBMREYDzE5OTkwMTAxMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUH -ADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIw -MjRlLmNybDAdBgNVHQ4EFgQUwT4r2UCtHWBnpnI3SjeaAGQyGlkwDgYDVR0PAQH/ -BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMGDy0L8r0KQxdmz7+6ZpzAtnoB7t6wP5 -YURwjJu5ysBMuYMIbw/5+R1Xv4nPo7BdQgIxAO3QXB8kg9w1jt8br7Sw2NUyUQZi -+Gt7a5Km+pxsMkiqCQYndI1Jrg/pW1gfUBRT2Q== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGqDCCBi6gAwIBAgIQDG5nZZQTInaS9mOLZwUmLDAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjUwOTA4MTIyNTIyWhcNMjgwOTA3MTIyNTIxWjBX +MQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzENMAsGA1UEBAwEVEVTVDEL +MAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkq +hkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjZXgGleu94rffU4c2iMM6J0eunUhbISt +ps5unECMwjKRohRJkwJlWToHv99Qs90vFM6rxMmZVKS6MZJ3WjkgbE0dxVVsWLnA +oBZWaZIidAgmQ6l0Cj0bLKjrRzKU/8T4gqy8tVrOJCvvwAlUIawzUdkQ+WdBCu79 +ipm2Lhh0KYkc20a9jNPfFwCOTkuO1AcVkhN6kdjIZgLrtlngKuhqNyotQmJ4Tl7+ +HkAtKV1zojIAY/LusoB5WWivdp6ey7hW4CyFAUhWpRnILcGeQ7sRBswQthEERThA +d5GRuiRWiz6eGrMAo7niINwoDNWwJDCeeAiJtYsjUBVyMyjxbPhX6DtwwZ3gP3NO +iwBQJ3qGDPNu0zGVdbph8+x9NZvkzAcPi8kr67S377BiH8AZPThzzkbadZxgIJrF +9WsTneKzAAk37HfGs3ESpFYaj8jlrI9RGJldh/nhTEdanqLhbGPO0gLXPJXR7pDG ++X9p6lC+sAa6mLVvBE5YSjQJoKCq8fsOfEQ0kyLcNem7rkxLh1ybsk57aDmZPPmx +Su853re+WvqJR8zPgD1qs7/LSKWeOOyi56OcVQ7Dmp6c56JH5JKQdUGbEXtkH+b6 +hHSMTXB/q/1OBwM4Tf7qO+AYU6MDVDdiEC0yAJm85b6cjbaxmY8a8eR2E3QuhV0H +7XLig9ebhoVb8cPJQmJxLMB1UHM4klQcav4F7+aaIOmEx60L0c7IITHU524LVjsP +GyqstXZxugSYh9V0h+aMdhtAj/JhWIh50fYrcaiyXZBu/nyRvzkY/6m5w/ycdiJ4 +tTNn+R5sJzdpo6SHQ5FnKRnpRnS7vrVSOy/quuYvvE06dE4fZrJfxAsGvg1L+qNT +yhpXECq9AF8U4Mqp03fnI5Dxglhx5rmNJZQKFM7PEDul0dV3lyAf6fmDPj/5H4L7 +RQyh5RcPEKbyk3Aw6v6RT69VuxXO2MvWDQx+S96Hk4105+BzWMWbOYyCWDRJz7IP +WTMQW5q1VnZTKElR1kxj6Ae7pITrlDDnAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAA +MB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQw +YjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k +ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIw +MjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1E +RU1PLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0 +cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlv +bi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsG +AQUFBwkBMREYDzE5OTkwMTAxMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUH +ADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIw +MjRlLmNybDAdBgNVHQ4EFgQUwT4r2UCtHWBnpnI3SjeaAGQyGlkwDgYDVR0PAQH/ +BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMGDy0L8r0KQxdmz7+6ZpzAtnoB7t6wP5 +YURwjJu5ysBMuYMIbw/5+R1Xv4nPo7BdQgIxAO3QXB8kg9w1jt8br7Sw2NUyUQZi ++Gt7a5Km+pxsMkiqCQYndI1Jrg/pW1gfUBRT2Q== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-cert-40504040001.pem.crt b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt index cfaeac6b..643909f7 100644 --- a/src/test/resources/test-certs/auth-cert-40504040001.pem.crt +++ b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQy -bEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7C -XahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEO -TnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5n -zCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcy -hPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY -0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29Jc -owTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943 -iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9Ps -mN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1Jg -xBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr -/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRj -Fisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QA -CBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq -8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v0 -9hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSg -At+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHx -MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYI -KwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJ -RC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5z -ay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQw -NTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsG -AQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9j -ZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1Ud -CQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0G -CysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rl -c3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHK -mTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0X -nPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxW -Cbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2 ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQy +bEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7C +XahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEO +TnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5n +zCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcy +hPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY +0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29Jc +owTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943 +iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9Ps +mN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1Jg +xBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr +/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRj +Fisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QA +CBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq +8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v0 +9hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSg +At+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHx +MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYI +KwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJ +RC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5z +ay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQw +NTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsG +AQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9j +ZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1Ud +CQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0G +CysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rl +c3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHK +mTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0X +nPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxW +Cbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2 +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt index 7df0ee18..9c3a45b6 100644 --- a/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt +++ b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt @@ -1,46 +1,46 @@ ------BEGIN CERTIFICATE----- -MIIIDTCCBfWgAwIBAgIQM34W+9hlpYRjtEHrK9yczDANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwIBcNMjMwMTAzMTQ1NTM5WhgPMjAzMDEyMTcyMzU5NTlaMGoxCzAJ -BgNVBAYTAkxWMRkwFwYDVQQDDBBURVNUTlVNQkVSLEFEVUxUMRMwEQYDVQQEDApU -RVNUTlVNQkVSMQ4wDAYDVQQqDAVBRFVMVDEbMBkGA1UEBRMSUE5PTFYtMDIwMTAw -LTI5OTkwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAp4yVDWinZexD -b/OUB7U414CuMbmzgo7ApXVML2sTU55XBbcQUCo15wkO9gP6u72YZ1TiEN1wbFxw -mBGGoVm5RRi/1WdQhSRcZ3TOPwE06VILayZ6kQo+D6WbqwazgWuCPYl858xe0E9k -PkEKeNhX3LgDVUFW9kq09HVPqJ2e4anw+eAF78bxjp2pN49IziZBsUjNjmcmartp -Qf5nFJEqIC3m+67oQoqkRtfTX7eKF5+pjoq6XzFgkwdp4Xnfq3wgN+fmqJF8tGeL -1jQxMqWWuqhwMUaklbW4s+M3vGVMQd5rDnBl5qRCPn+gNxseKAaueUchI2WGjOKU -QM7MSay0qeYd3s435cRwGub7asY6p/7Nn19ykQDPj9bV35eZ1GLL7Sb/QYXBUTcP -fd1lxjJwNYJIbpJ0Aj/qtgmGGbWa2ROs9v2l2jh8Re2YRaIvYVrVTiCHFOuMf+PH -O36qFOWdbGtZnYT3QEPQVe0DCXhsioMndEo/BSASGmgriqBaS+T6x9UcS6qbRkQ5 -m0piwaDrwaBSyP4oW60jbLA9SvWDancGOLXqOgEIyhdxuB3w0TFLytjZMq0olLjy -XeK6XL1A052CBu97/8GmZkTelg9qYMJmrVUaidby16bYUgjxSV9O+0w+ZzVDUuwE -p5LUQwoEBrylA2fD8gk9JhffsbbmrgvEOCk8f590KRt4JczND+WmmaXGTNv9Rbj2 -9pv0wqlpMV8iaPxxvdNydlE51K9baxopZibZdH3abiRhsyffDZckZHNIzExzE/V9 -6MYh9Myos8dDSR+HPg3F1i92PjsusJUMUyZFcs0cAWXV0Z/4G7yB4J1YeJx6GkbD -PLG8G1kvnh1mJAxmm/39MK3gHINIe1+JNc8c32Y75JDF4yjQftP0lwHx/WHwfvkq -nmLHYc9Sb2o/AYjbTsxOxqhnmkS/smn4R/t3/i4gzfvGnhc9Mh/5OduGeTWAP4CE -M6pgsCRlcGNwRNTzw3yW4nNZdFIsINru6Gtwy3PlQzuNjT9lvgt9AgMBAAGjggGt -MIIBqTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBcBgNVHSAEVTBTMEcGCisG -AQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1 -L2VuL3JlcG9zaXRvcnkvQ1BTLzAIBgYEAI96AQIwHQYDVR0OBBYEFBEOFq9GzHMr -rV6qgBoEw1Y9lEquMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MBMG -A1UdJQQMMAoGCCsGAQUFBwMCMHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYd -aHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6 -Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0 -MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMjAxMDAtMjk5OTAtTU9D -Sy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwMDAxMDIxMjAwMDBaMA0G -CSqGSIb3DQEBCwUAA4ICAQCwZnf+COHoIlkxIFPU33TCmh//k5oVDp42pK7Zp765 -KIppDRuxVtFHNvM5F4P9lGQ1FycPi+8N6uDX+XboKQ5SwtvcKYL23GfQwxnzMc0h -lyFm9Gx5Etl200CIP0hTCiFpEWouIOy3spGXwoaFjrcL+oYuL0HW7kFORSjerwOL -osTHJsT1geDY64INgO98i7WgHnmtMjoVXeyklVCsKwvYnMZVFzrpQkL7h9CQGffq -4S664jGYEghnZXh8uiq8x3l4V777NPuwCspiebXUrEmUe9lP/dHwaX009y4gygkB -VQSr7z8QTh3Cbt2g9Brt+w/PqKmYw+eJyQ21DbxPrQyZKQvFY+XsWkyWOFrRPsG+ -Rb7lqvejm8ppqQX7wH6ulvhkKT91vF1uy2icb77VB5i3m7LMSZF7BUa/U6Qlm2GK -Cz8+6FN3xiC+cMulWaMA7c0tiT9aWTqqPh5w9RfAIWXgbsG0vP+vSMtyRERcMpA+ -hjzB6Rj44j0Mg4PfKpvlsYG2aF5NXNRJpT2utm+Var44+HthQkltoWhGjXG9Rc7c -MQBRECohUqeNtV0GCQUnE2RtZvufidVw4sOx3qxmoU/dlribucQ4mVPZjVF3LVoX -IYJRUqtdMrh9dR9ZDAwuA1Y6sxr3Dw/QQujtU8NKAAGKIPsPVAir5Dfs7R+bF/dR -mg== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIIDTCCBfWgAwIBAgIQM34W+9hlpYRjtEHrK9yczDANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMjMwMTAzMTQ1NTM5WhgPMjAzMDEyMTcyMzU5NTlaMGoxCzAJ +BgNVBAYTAkxWMRkwFwYDVQQDDBBURVNUTlVNQkVSLEFEVUxUMRMwEQYDVQQEDApU +RVNUTlVNQkVSMQ4wDAYDVQQqDAVBRFVMVDEbMBkGA1UEBRMSUE5PTFYtMDIwMTAw +LTI5OTkwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAp4yVDWinZexD +b/OUB7U414CuMbmzgo7ApXVML2sTU55XBbcQUCo15wkO9gP6u72YZ1TiEN1wbFxw +mBGGoVm5RRi/1WdQhSRcZ3TOPwE06VILayZ6kQo+D6WbqwazgWuCPYl858xe0E9k +PkEKeNhX3LgDVUFW9kq09HVPqJ2e4anw+eAF78bxjp2pN49IziZBsUjNjmcmartp +Qf5nFJEqIC3m+67oQoqkRtfTX7eKF5+pjoq6XzFgkwdp4Xnfq3wgN+fmqJF8tGeL +1jQxMqWWuqhwMUaklbW4s+M3vGVMQd5rDnBl5qRCPn+gNxseKAaueUchI2WGjOKU +QM7MSay0qeYd3s435cRwGub7asY6p/7Nn19ykQDPj9bV35eZ1GLL7Sb/QYXBUTcP +fd1lxjJwNYJIbpJ0Aj/qtgmGGbWa2ROs9v2l2jh8Re2YRaIvYVrVTiCHFOuMf+PH +O36qFOWdbGtZnYT3QEPQVe0DCXhsioMndEo/BSASGmgriqBaS+T6x9UcS6qbRkQ5 +m0piwaDrwaBSyP4oW60jbLA9SvWDancGOLXqOgEIyhdxuB3w0TFLytjZMq0olLjy +XeK6XL1A052CBu97/8GmZkTelg9qYMJmrVUaidby16bYUgjxSV9O+0w+ZzVDUuwE +p5LUQwoEBrylA2fD8gk9JhffsbbmrgvEOCk8f590KRt4JczND+WmmaXGTNv9Rbj2 +9pv0wqlpMV8iaPxxvdNydlE51K9baxopZibZdH3abiRhsyffDZckZHNIzExzE/V9 +6MYh9Myos8dDSR+HPg3F1i92PjsusJUMUyZFcs0cAWXV0Z/4G7yB4J1YeJx6GkbD +PLG8G1kvnh1mJAxmm/39MK3gHINIe1+JNc8c32Y75JDF4yjQftP0lwHx/WHwfvkq +nmLHYc9Sb2o/AYjbTsxOxqhnmkS/smn4R/t3/i4gzfvGnhc9Mh/5OduGeTWAP4CE +M6pgsCRlcGNwRNTzw3yW4nNZdFIsINru6Gtwy3PlQzuNjT9lvgt9AgMBAAGjggGt +MIIBqTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBcBgNVHSAEVTBTMEcGCisG +AQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1 +L2VuL3JlcG9zaXRvcnkvQ1BTLzAIBgYEAI96AQIwHQYDVR0OBBYEFBEOFq9GzHMr +rV6qgBoEw1Y9lEquMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MBMG +A1UdJQQMMAoGCCsGAQUFBwMCMHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYd +aHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6 +Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0 +MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMjAxMDAtMjk5OTAtTU9D +Sy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwMDAxMDIxMjAwMDBaMA0G +CSqGSIb3DQEBCwUAA4ICAQCwZnf+COHoIlkxIFPU33TCmh//k5oVDp42pK7Zp765 +KIppDRuxVtFHNvM5F4P9lGQ1FycPi+8N6uDX+XboKQ5SwtvcKYL23GfQwxnzMc0h +lyFm9Gx5Etl200CIP0hTCiFpEWouIOy3spGXwoaFjrcL+oYuL0HW7kFORSjerwOL +osTHJsT1geDY64INgO98i7WgHnmtMjoVXeyklVCsKwvYnMZVFzrpQkL7h9CQGffq +4S664jGYEghnZXh8uiq8x3l4V777NPuwCspiebXUrEmUe9lP/dHwaX009y4gygkB +VQSr7z8QTh3Cbt2g9Brt+w/PqKmYw+eJyQ21DbxPrQyZKQvFY+XsWkyWOFrRPsG+ +Rb7lqvejm8ppqQX7wH6ulvhkKT91vF1uy2icb77VB5i3m7LMSZF7BUa/U6Qlm2GK +Cz8+6FN3xiC+cMulWaMA7c0tiT9aWTqqPh5w9RfAIWXgbsG0vP+vSMtyRERcMpA+ +hjzB6Rj44j0Mg4PfKpvlsYG2aF5NXNRJpT2utm+Var44+HthQkltoWhGjXG9Rc7c +MQBRECohUqeNtV0GCQUnE2RtZvufidVw4sOx3qxmoU/dlribucQ4mVPZjVF3LVoX +IYJRUqtdMrh9dR9ZDAwuA1Y6sxr3Dw/QQujtU8NKAAGKIPsPVAir5Dfs7R+bF/dR +mg== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/ca-cert.pem.crt b/src/test/resources/test-certs/ca-cert.pem.crt index 5ca038c0..b6d7af73 100644 --- a/src/test/resources/test-certs/ca-cert.pem.crt +++ b/src/test/resources/test-certs/ca-cert.pem.crt @@ -1,23 +1,23 @@ ------BEGIN CERTIFICATE----- -MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw -bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s -dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow -cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx -FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv -bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 -oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh -vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC -AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L -gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr -LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo -dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw -VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv -dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy -MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j -cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB -BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f -4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo -XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa -7Wy8pf2lw6EcfyU= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert index e27cb966..b49f0d45 100644 --- a/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert +++ b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert @@ -1,42 +1,42 @@ ------BEGIN CERTIFICATE----- -MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ -q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw -SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL -r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma -tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB -0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W -rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY -1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp -RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH -48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 -nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk -aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e -wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk -pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM -ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f -jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY -xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC -izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG -CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u -c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 -MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD -VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF -BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA -jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov -L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 -c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov -L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU -1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 -7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC -MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld -CQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/expired-cert.pem.crt b/src/test/resources/test-certs/expired-cert.pem.crt index 8e18defd..03d919f4 100644 --- a/src/test/resources/test-certs/expired-cert.pem.crt +++ b/src/test/resources/test-certs/expired-cert.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkG -A1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYD -VQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ -Tk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0G -CSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51ISh -Sj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+Ml -lSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5cz -GP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdc -DmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncv -pRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhN -BYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04 -nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a -4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2 -wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSno -JPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoA -xoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIE -sDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6 -Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4E -FgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtm -Vf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkG -CCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEF -BQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tf -MjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdF -TOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDi -G7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbr -rl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxM -TEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl -4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1 -ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y5 -2n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf -83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAK -Gn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbR -R1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2P -MnXRUgtlnV7ykFpmkR4JcQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkG +A1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYD +VQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0G +CSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51ISh +Sj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+Ml +lSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5cz +GP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdc +DmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncv +pRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhN +BYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04 +nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a +4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2 +wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSno +JPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoA +xoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIE +sDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6 +Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4E +FgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtm +Vf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkG +CCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEF +BQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tf +MjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdF +TOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDi +G7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbr +rl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxM +TEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl +4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1 +ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y5 +2n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf +83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAK +Gn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbR +R1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2P +MnXRUgtlnV7ykFpmkR4JcQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/nq-auth-cert-40504049999.crt b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt index dd4d987b..1f941975 100644 --- a/src/test/resources/test-certs/nq-auth-cert-40504049999.crt +++ b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGjjCCBhSgAwIBAgIQM1MMPZyR0fwWexCl4aAn6zAKBggqhkjOPQQDAzByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMB4XDTI0MTAxNTE4NDMxOFoXDTI3MTAxNTE4NDMxN1ow -YzELMAkGA1UEBhMCTFQxFjAUBgNVBAMMDVRFU1ROVU1CRVIsT0sxEzARBgNVBAQM -ClRFU1ROVU1CRVIxCzAJBgNVBCoMAk9LMRowGAYDVQQFExFQTk9MVC00MDUwNDA0 -OTk5OTCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAcLdOmpoXwhh41xRM -MqkTl7EVlD7kFgQxX32yVKDGG1wIB/iEaIlc/JRmPwpPVV996gbaBsOmsTSkdhyo -QuqLiov/gQ8a5/ByKBgGxLflCgtV6SsVqzJZoPBhs0KiVKwTWgA2aLv+oaKcDbrv -40Tz5J0DrgevAQctYm6eM1plJ1n/J5sINFgdXHv1k6iqSCMKb35vLZiywHoXo11H -ERX6HcfoYgaBcuWTTv5c6XS4nbfl2JzEsXpzshifRLwU/gQvkHqWiUv3TwpaRVrP -/ClzJP+s8fwx4mGgW0J4tSQqt4HWLpZOEu4ksIKijbQc7wLN006cV1O8EkZZhXud -bJWS3JJtmiFMwMc6fWa07UuMYHho86jZkKbJkhgQFX4TNzSm3O3CABNaVvqElagp -4OMYBbbe0HPPlEWGyoApvxVHaWa2mwtKomjZw11BuqZf7HevAKhURbOV8ImMzxii -woTfsBSyHniDW/mOy5hfgtkrvpXsy0BQ6NjdMcPLGW1Zp5DgrzjxXQhpJDzBUrna -eBQjKhh51a/siuby6eblm73gBHVLzDRK1Im0l4als5Lw/xoc8exlhFJhfg7+9L9a -NfUQAJWnUJ0cM3fd9O/PqqWlbk/R/1RUuIIhlmPhiPO+s/CWPZqbOIiasP1AyQMQ -N5p2+KRIQ9/RmoGz+fEzk063wR62YUms1iU7GTqqjSsficdVAfsnOQhsVwUXfOJx -ob2UYOh/RzEB9Uo+Mhwu4Omgxgl8PIL3nj/Lj8KmbdvUeAB9JgZ2xc5gbPY4GL4e -ShcyuxaNM4T/4N08WarBExI5GwJgqgaYA/J3agrqe9tcZJjlBPFT1A/O5Lc3Ewn8 -xlSvWe3r695PLf73ADi/vhjhmznqp72AmMYfmaj/WecS0FSfRPQP7ovb1N5DjIEx -U6GNF1Lb/9Q/z56uMf2y0tU35F4y/zhnBdZ+eSa69DoKD49RHRjMTA+UJNM4DZrN -uBszXI30OmlvGYZdzETQU52CpR79E2svr1hMto2G2/TA4YzzAgMBAAGjggHPMIIB -yzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIG -CCsGAQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtTlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1v -LnNrLmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0xU -LTQwNTA0MDQ5OTk5LU1PQ0stTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBU -BggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJj -ZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATAW -BgNVHSUEDzANBgsrBgEEAYPmYgUHADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8v -Yy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5jcmwwHQYDVR0OBBYEFBetuv93k5ps -ZBo46kmtdw4TGe24MA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAwNoADBlAjEA -llFf3tx3m9UVEhCmg3MFBtkD7rCaK6MSym/DEhR7LfXXOIWEuVAC0eaX8T2DyXxw -AjB1WKZRYM6awmjUpt3CdRSbQ0DcZbpWxjYjuHM3zkt2XkDwvXwEcfJbnnetIDDT -tSE= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGjjCCBhSgAwIBAgIQM1MMPZyR0fwWexCl4aAn6zAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI0MTAxNTE4NDMxOFoXDTI3MTAxNTE4NDMxN1ow +YzELMAkGA1UEBhMCTFQxFjAUBgNVBAMMDVRFU1ROVU1CRVIsT0sxEzARBgNVBAQM +ClRFU1ROVU1CRVIxCzAJBgNVBCoMAk9LMRowGAYDVQQFExFQTk9MVC00MDUwNDA0 +OTk5OTCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAcLdOmpoXwhh41xRM +MqkTl7EVlD7kFgQxX32yVKDGG1wIB/iEaIlc/JRmPwpPVV996gbaBsOmsTSkdhyo +QuqLiov/gQ8a5/ByKBgGxLflCgtV6SsVqzJZoPBhs0KiVKwTWgA2aLv+oaKcDbrv +40Tz5J0DrgevAQctYm6eM1plJ1n/J5sINFgdXHv1k6iqSCMKb35vLZiywHoXo11H +ERX6HcfoYgaBcuWTTv5c6XS4nbfl2JzEsXpzshifRLwU/gQvkHqWiUv3TwpaRVrP +/ClzJP+s8fwx4mGgW0J4tSQqt4HWLpZOEu4ksIKijbQc7wLN006cV1O8EkZZhXud +bJWS3JJtmiFMwMc6fWa07UuMYHho86jZkKbJkhgQFX4TNzSm3O3CABNaVvqElagp +4OMYBbbe0HPPlEWGyoApvxVHaWa2mwtKomjZw11BuqZf7HevAKhURbOV8ImMzxii +woTfsBSyHniDW/mOy5hfgtkrvpXsy0BQ6NjdMcPLGW1Zp5DgrzjxXQhpJDzBUrna +eBQjKhh51a/siuby6eblm73gBHVLzDRK1Im0l4als5Lw/xoc8exlhFJhfg7+9L9a +NfUQAJWnUJ0cM3fd9O/PqqWlbk/R/1RUuIIhlmPhiPO+s/CWPZqbOIiasP1AyQMQ +N5p2+KRIQ9/RmoGz+fEzk063wR62YUms1iU7GTqqjSsficdVAfsnOQhsVwUXfOJx +ob2UYOh/RzEB9Uo+Mhwu4Omgxgl8PIL3nj/Lj8KmbdvUeAB9JgZ2xc5gbPY4GL4e +ShcyuxaNM4T/4N08WarBExI5GwJgqgaYA/J3agrqe9tcZJjlBPFT1A/O5Lc3Ewn8 +xlSvWe3r695PLf73ADi/vhjhmznqp72AmMYfmaj/WecS0FSfRPQP7ovb1N5DjIEx +U6GNF1Lb/9Q/z56uMf2y0tU35F4y/zhnBdZ+eSa69DoKD49RHRjMTA+UJNM4DZrN +uBszXI30OmlvGYZdzETQU52CpR79E2svr1hMto2G2/TA4YzzAgMBAAGjggHPMIIB +yzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIG +CCsGAQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtTlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1v +LnNrLmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0xU +LTQwNTA0MDQ5OTk5LU1PQ0stTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBU +BggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJj +ZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATAW +BgNVHSUEDzANBgsrBgEEAYPmYgUHADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8v +Yy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5jcmwwHQYDVR0OBBYEFBetuv93k5ps +ZBo46kmtdw4TGe24MA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAwNoADBlAjEA +llFf3tx3m9UVEhCmg3MFBtkD7rCaK6MSym/DEhR7LfXXOIWEuVAC0eaX8T2DyXxw +AjB1WKZRYM6awmjUpt3CdRSbQ0DcZbpWxjYjuHM3zkt2XkDwvXwEcfJbnnetIDDT +tSE= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/nq-signing-cert.pem b/src/test/resources/test-certs/nq-signing-cert.pem index d1aedc43..9713a2e9 100644 --- a/src/test/resources/test-certs/nq-signing-cert.pem +++ b/src/test/resources/test-certs/nq-signing-cert.pem @@ -1,37 +1,37 @@ ------BEGIN CERTIFICATE----- -MIIGdDCCBfmgAwIBAgIQayQDbT+81MKPikMhkxHiEjAKBggqhkjOPQQDAzByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMB4XDTI1MDgzMTA1NTkyNVoXDTI4MDgzMDA1NTkyNFow -XzELMAkGA1UEBhMCRkkxFDASBgNVBAMMC01VU0VSLFVSTUFTMQ4wDAYDVQQEDAVN -VVNFUjEOMAwGA1UEKgwFVVJNQVMxGjAYBgNVBAUTEVBOT0ZJLTM5MDAzMDEyNzk4 -MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAmX7GLKF5e8LxXrRNl5sM -AHA3UwM9/RKIjAePphFJ1IwEu4f1NutqGWs9hhsh6PnBnbnKsvN/US//5Bw/t37z -gBqgQaToPCww9zHsUFl0w3Q+XGxK3wzKltu50WaxIQaLExFTgFsPmViabT7M1Kqu -6UYlJQJQPkgkfvmuz3LeCFXFRpjEoIgvVfbAYxXfn8V1HPwPAhFTkC1iTht14SOE -WolghzV5R3IYeCey8Y978rFy24avN+ea1aweti98UaFH8wIjuX1zND7SHY2fu5tF -+uxSacJuBUukL0w34n3ODEPDJXojPEnJEIOZmJIV35jGcTMEc6OVlKHYBfwISUjy -+/dIy+oq/qz7z5Kr4gv2DBTLpEj6bCgS2uDDxVXZbDFY3K0jXtxVtk11pWKFUEtd -YJFV6FRswFHF/WYp8o2WJ7O0hlB47pkUgLtKXLNJrS8z+dLcP3s626ZSjfEPf3LD -Ku9GoDetBzvj+QanNUpzZGpWdaMNN4dpLdMSyp2WK6KK3TXowmYYSaXS2t+5MyM9 -0Om3JUkY9hIBIfeAgGwA9Vx8OT5srYU1zWIGcP8AeTsE2JDpCh11M56dAw8yF2z0 -JyTjYcmmX/Zla3gT76yhL43AHT64hxohpiZr8R2lKSFO1qg7ECly1XKov5Vm8rTd -CaDiWLrBwh4+XEJSludG6KZ+0Q8hrGU23qNBH/Yo4xh2vArwdaoKpHUiMbwu9/MZ -k7WTgCqxnp9fC6l+25ePfK4xiA5iVfZsmBs8iE8Z/J3akd2tZRW3jE672F1L+V6m -icbVCi8VFpWcVfdTZFvQPoozZAx8dm//L4hEEeu/7ylEmrn4luEcMsk386Pj61KB -wk54zzuxWc9i3u4JyW3B6F87LhGv01osVK35mXWhBI5hN8YrRHieHafuMGrfdGp9 -XrKRleYtih2BYr+FD2bfaxf0KEBaQdHMXEZ814x3SYLb1iv7/5yz6FNB37OwlIwa -VvoxaIggGXI0Ht0axQ6dqvDdo1cH+uXNYZJtp+ScQ6qVAgMBAAGjggG3MIIBszAJ -BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIGCCsG -AQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9FSUQt -TlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1vLnNr -LmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0ZJLTM5 -MDAzMDEyNzk4LTlUMTctTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATA1BgNV -HR8ELjAsMCqgKKAmhiRodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5j -cmwwHQYDVR0OBBYEFMiBd4urSvDE0mt9B6CBjlPj8RMcMA4GA1UdDwEB/wQEAwIG -QDAKBggqhkjOPQQDAwNpADBmAjEA/dDkzO4iuKidm3IP4j+5JKOrzhn1+XO7WzbM -Uvu3+3Wn0zUAg88I0tAFPUHUsVqrAjEAzlmPXUdhTGEH7dGQowFHVnsSUP4o0Q/f -qqcQOidwglE0899fEZoQSLHJ6tR0K7ip ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGdDCCBfmgAwIBAgIQayQDbT+81MKPikMhkxHiEjAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI1MDgzMTA1NTkyNVoXDTI4MDgzMDA1NTkyNFow +XzELMAkGA1UEBhMCRkkxFDASBgNVBAMMC01VU0VSLFVSTUFTMQ4wDAYDVQQEDAVN +VVNFUjEOMAwGA1UEKgwFVVJNQVMxGjAYBgNVBAUTEVBOT0ZJLTM5MDAzMDEyNzk4 +MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAmX7GLKF5e8LxXrRNl5sM +AHA3UwM9/RKIjAePphFJ1IwEu4f1NutqGWs9hhsh6PnBnbnKsvN/US//5Bw/t37z +gBqgQaToPCww9zHsUFl0w3Q+XGxK3wzKltu50WaxIQaLExFTgFsPmViabT7M1Kqu +6UYlJQJQPkgkfvmuz3LeCFXFRpjEoIgvVfbAYxXfn8V1HPwPAhFTkC1iTht14SOE +WolghzV5R3IYeCey8Y978rFy24avN+ea1aweti98UaFH8wIjuX1zND7SHY2fu5tF ++uxSacJuBUukL0w34n3ODEPDJXojPEnJEIOZmJIV35jGcTMEc6OVlKHYBfwISUjy ++/dIy+oq/qz7z5Kr4gv2DBTLpEj6bCgS2uDDxVXZbDFY3K0jXtxVtk11pWKFUEtd +YJFV6FRswFHF/WYp8o2WJ7O0hlB47pkUgLtKXLNJrS8z+dLcP3s626ZSjfEPf3LD +Ku9GoDetBzvj+QanNUpzZGpWdaMNN4dpLdMSyp2WK6KK3TXowmYYSaXS2t+5MyM9 +0Om3JUkY9hIBIfeAgGwA9Vx8OT5srYU1zWIGcP8AeTsE2JDpCh11M56dAw8yF2z0 +JyTjYcmmX/Zla3gT76yhL43AHT64hxohpiZr8R2lKSFO1qg7ECly1XKov5Vm8rTd +CaDiWLrBwh4+XEJSludG6KZ+0Q8hrGU23qNBH/Yo4xh2vArwdaoKpHUiMbwu9/MZ +k7WTgCqxnp9fC6l+25ePfK4xiA5iVfZsmBs8iE8Z/J3akd2tZRW3jE672F1L+V6m +icbVCi8VFpWcVfdTZFvQPoozZAx8dm//L4hEEeu/7ylEmrn4luEcMsk386Pj61KB +wk54zzuxWc9i3u4JyW3B6F87LhGv01osVK35mXWhBI5hN8YrRHieHafuMGrfdGp9 +XrKRleYtih2BYr+FD2bfaxf0KEBaQdHMXEZ814x3SYLb1iv7/5yz6FNB37OwlIwa +VvoxaIggGXI0Ht0axQ6dqvDdo1cH+uXNYZJtp+ScQ6qVAgMBAAGjggG3MIIBszAJ +BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIGCCsG +AQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9FSUQt +TlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1vLnNr +LmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0ZJLTM5 +MDAzMDEyNzk4LTlUMTctTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATA1BgNV +HR8ELjAsMCqgKKAmhiRodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5j +cmwwHQYDVR0OBBYEFMiBd4urSvDE0mt9B6CBjlPj8RMcMA4GA1UdDwEB/wQEAwIG +QDAKBggqhkjOPQQDAwNpADBmAjEA/dDkzO4iuKidm3IP4j+5JKOrzhn1+XO7WzbM +Uvu3+3Wn0zUAg88I0tAFPUHUsVqrAjEAzlmPXUdhTGEH7dGQowFHVnsSUP4o0Q/f +qqcQOidwglE0899fEZoQSLHJ6tR0K7ip +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/other-auth-cert.pem.crt b/src/test/resources/test-certs/other-auth-cert.pem.crt index 5ad5e65e..e8f5b8a9 100644 --- a/src/test/resources/test-certs/other-auth-cert.pem.crt +++ b/src/test/resources/test-certs/other-auth-cert.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcw -FQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ -Tk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTAL -BgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEw -DQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnU -hKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz -4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6z -lzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8 -l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpe -dy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0U -aE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0w -LTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2n -T5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339z -t7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKx -Kegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XK -ygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD -AgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRw -czovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1Ud -DgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XM -C2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4w -KQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsG -AQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNL -XzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifm -rjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2p -Kmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6v -ZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7 -grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4 -lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3Fa -YpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8D -j/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5o -PEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5 -m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql4 -40sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytf -q8s5bZci5vnHm110lnPhQxM= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcw +FQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTAL +BgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEw +DQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnU +hKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz +4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6z +lzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8 +l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpe +dy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0U +aE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0w +LTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2n +T5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339z +t7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKx +Kegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XK +ygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD +AgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRw +czovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1Ud +DgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XM +C2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4w +KQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsG +AQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNL +XzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifm +rjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2p +Kmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6v +ZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7 +grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4 +lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3Fa +YpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8D +j/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5o +PEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5 +m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql4 +40sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytf +q8s5bZci5vnHm110lnPhQxM= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/sign-cert-40504040001.pem.crt b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt index e27cb966..b49f0d45 100644 --- a/src/test/resources/test-certs/sign-cert-40504040001.pem.crt +++ b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt @@ -1,42 +1,42 @@ ------BEGIN CERTIFICATE----- -MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ -q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw -SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL -r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma -tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB -0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W -rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY -1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp -RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH -48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 -nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk -aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e -wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk -pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM -ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f -jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY -xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC -izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG -CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u -c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 -MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD -VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF -BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA -jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov -L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 -c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov -L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU -1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 -7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC -MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld -CQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt index ae2033e6..2572e4c8 100644 --- a/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt +++ b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt @@ -1,22 +1,22 @@ ------BEGIN CERTIFICATE----- -MIIDpTCCAwagAwIBAgIQTiO7d7Wr6Flg+BPaeYgVHDAKBggqhkjOPQQDAzBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwHhcNMjEwNzIxMTIzMjI2WhcNMzYwNzIxMTIzMjI2WjByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBn6bE+DVXUwO -8gYWoA6tu2gb4ou3Gk55ge6jYehcxehS5RO3GaknTrc2YrLcq6nwrcBoIrkVlDOd -Bfub4oea3zL7VlA/ADQ8PTYexu+0zxk1TEtsj0KHH9lh8f7FR1awo4IBYzCCAV8w -HwYDVR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFLNZ0LWq -a/mBsLQHo63DzpXv8Y5GMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/ -AgEAMGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2su -ZWUvb2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09U -X0cxXzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2su -ZWUvVEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAw -PTA7BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9y -ZXBvc2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYwAMIGIAkIBsJ6X9zwyHP3b28br -WIsid0vqWxOzPFU4GFTH/AqXW71V9WLNBJHsbuBg2VNi4k7CKUW7MpRqL8UI8QX7 -/X7jFxMCQgF+IPUDMXMsV99sgqo/Y6VkZYqiakayHkvECkJCncUfmpqVYUlcAxeZ -zRlYIOz3F5AvYJTrtMP0TR3yASD1GtYs4A== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDpTCCAwagAwIBAgIQTiO7d7Wr6Flg+BPaeYgVHDAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzIxMTIzMjI2WhcNMzYwNzIxMTIzMjI2WjByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBn6bE+DVXUwO +8gYWoA6tu2gb4ou3Gk55ge6jYehcxehS5RO3GaknTrc2YrLcq6nwrcBoIrkVlDOd +Bfub4oea3zL7VlA/ADQ8PTYexu+0zxk1TEtsj0KHH9lh8f7FR1awo4IBYzCCAV8w +HwYDVR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFLNZ0LWq +a/mBsLQHo63DzpXv8Y5GMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/ +AgEAMGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2su +ZWUvb2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09U +X0cxXzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2su +ZWUvVEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAw +PTA7BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9y +ZXBvc2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYwAMIGIAkIBsJ6X9zwyHP3b28br +WIsid0vqWxOzPFU4GFTH/AqXW71V9WLNBJHsbuBg2VNi4k7CKUW7MpRqL8UI8QX7 +/X7jFxMCQgF+IPUDMXMsV99sgqo/Y6VkZYqiakayHkvECkJCncUfmpqVYUlcAxeZ +zRlYIOz3F5AvYJTrtMP0TR3yASD1GtYs4A== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt index 5ca038c0..b6d7af73 100644 --- a/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt +++ b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt @@ -1,23 +1,23 @@ ------BEGIN CERTIFICATE----- -MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw -bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s -dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow -cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx -FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv -bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 -oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh -vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC -AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L -gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr -LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo -dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw -VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv -dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy -MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j -cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB -BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f -4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo -XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa -7Wy8pf2lw6EcfyU= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE----- From bc6157305aea08186e56e6484390ffa8065f5b65 Mon Sep 17 00:00:00 2001 From: Kimmo Lillipuu Date: Tue, 14 Oct 2025 20:20:28 +0300 Subject: [PATCH 57/57] Revert "Github workflow updates" This reverts commit 37ce2cf8ec341065056f33448857e612a3272d0f. --- .github/workflows/check.yaml | 80 +- .github/workflows/publish.yaml | 128 +- .github/workflows/tests.yaml | 72 +- .gitignore | 12 +- .mvn/wrapper/MavenWrapperDownloader.java | 234 +- .mvn/wrapper/maven-wrapper.properties | 36 +- CHANGELOG.md | 650 +-- DEVELOPERS.md | 42 +- LICENSE | 42 +- LICENSE.3RD-PARTY | 228 +- MIGRATION_GUIDE.md | 150 +- README.md | 3072 +++++++------- mvnw | 632 +-- mvnw.cmd | 376 +- pom.xml | 600 +-- src/license/LICENSE.EPL-1.0 | 408 +- src/license/third-party-file-template.ftl | 42 +- .../AuthenticationCertificateLevel.java | 142 +- .../ee/sk/smartid/AuthenticationIdentity.java | 370 +- .../smartid/AuthenticationIdentityMapper.java | 136 +- .../ee/sk/smartid/AuthenticationResponse.java | 534 +-- .../smartid/AuthenticationResponseMapper.java | 94 +- .../AuthenticationResponseMapperImpl.java | 556 +-- ...ificateByDocumentNumberRequestBuilder.java | 380 +- .../CertificateByDocumentNumberResult.java | 76 +- .../sk/smartid/CertificateChoiceResponse.java | 280 +- .../CertificateChoiceResponseValidator.java | 336 +- .../java/ee/sk/smartid/CertificateLevel.java | 154 +- .../java/ee/sk/smartid/CertificateParser.java | 146 +- .../java/ee/sk/smartid/CertificateState.java | 114 +- .../ee/sk/smartid/CertificateValidator.java | 100 +- .../sk/smartid/CertificateValidatorImpl.java | 212 +- .../sk/smartid/DefaultTrustedCACertStore.java | 172 +- .../smartid/DefaultTrustedCAStoreBuilder.java | 316 +- ...ceLinkAuthenticationResponseValidator.java | 430 +- ...nkAuthenticationSessionRequestBuilder.java | 722 ++-- .../java/ee/sk/smartid/DeviceLinkBuilder.java | 722 ++-- ...ertificateChoiceSessionRequestBuilder.java | 424 +- ...iceLinkSignatureSessionRequestBuilder.java | 710 ++-- .../java/ee/sk/smartid/DeviceLinkType.java | 126 +- .../java/ee/sk/smartid/DigestCalculator.java | 122 +- src/main/java/ee/sk/smartid/DigestInput.java | 104 +- .../ee/sk/smartid/ErrorResultHandler.java | 194 +- .../sk/smartid/FileTrustedCAStoreBuilder.java | 448 +-- src/main/java/ee/sk/smartid/FlowType.java | 194 +- .../java/ee/sk/smartid/HashAlgorithm.java | 204 +- ...icationSignatureSessionRequestBuilder.java | 560 +-- .../java/ee/sk/smartid/MaskGenAlgorithm.java | 160 +- ...dSignatureCertificatePurposeValidator.java | 112 +- ...cationAuthenticationResponseValidator.java | 376 +- ...onAuthenticationSessionRequestBuilder.java | 630 +-- ...ertificateChoiceSessionRequestBuilder.java | 390 +- ...icationSignatureSessionRequestBuilder.java | 676 ++-- .../java/ee/sk/smartid/QrCodeGenerator.java | 284 +- ...dSignatureCertificatePurposeValidator.java | 256 +- src/main/java/ee/sk/smartid/RpChallenge.java | 110 +- .../ee/sk/smartid/RpChallengeGenerator.java | 148 +- .../ee/sk/smartid/RsaSsaPssParameters.java | 280 +- src/main/java/ee/sk/smartid/SessionType.java | 122 +- src/main/java/ee/sk/smartid/SignableData.java | 186 +- src/main/java/ee/sk/smartid/SignableHash.java | 174 +- .../ee/sk/smartid/SignatureAlgorithm.java | 164 +- .../SignatureCertificatePurposeValidator.java | 92 +- ...ureCertificatePurposeValidatorFactory.java | 82 +- ...ertificatePurposeValidatorFactoryImpl.java | 102 +- .../java/ee/sk/smartid/SignatureProtocol.java | 86 +- .../java/ee/sk/smartid/SignatureResponse.java | 546 +-- .../smartid/SignatureResponseValidator.java | 680 ++-- .../sk/smartid/SignatureValueValidator.java | 102 +- .../smartid/SignatureValueValidatorImpl.java | 208 +- .../java/ee/sk/smartid/SmartIdClient.java | 824 ++-- src/main/java/ee/sk/smartid/TrailerField.java | 162 +- .../ee/sk/smartid/TrustedCACertStore.java | 118 +- .../smartid/VerificationCodeCalculator.java | 130 +- .../ee/sk/smartid/VerificationCodeType.java | 100 +- ...enticationCertificatePurposeValidator.java | 92 +- ...ionCertificatePurposeValidatorFactory.java | 86 +- ...ertificatePurposeValidatorFactoryImpl.java | 100 +- ...enticationCertificatePurposeValidator.java | 110 +- ...enticationCertificatePurposeValidator.java | 154 +- .../ee/sk/smartid/common/InteractionType.java | 94 +- .../smartid/common/InteractionValidator.java | 110 +- .../sk/smartid/common/InteractionsMapper.java | 124 +- .../sk/smartid/common/SmartIdInteraction.java | 108 +- ...nQualifiedSmartIdCertificateValidator.java | 144 +- ...tIdAuthenticationCertificateValidator.java | 226 +- .../common/devicelink/CallbackUrl.java | 76 +- .../devicelink/UrlSafeTokenGenerator.java | 170 +- .../interactions/DeviceLinkInteraction.java | 174 +- .../DeviceLinkInteractionType.java | 124 +- .../interactions/NotificationInteraction.java | 196 +- .../NotificationInteractionType.java | 132 +- .../exception/EnduringSmartIdException.java | 110 +- .../exception/SessionNotFoundException.java | 66 +- .../SessionSecretMismatchException.java | 84 +- .../smartid/exception/SmartIdException.java | 110 +- ...UnprocessableSmartIdResponseException.java | 110 +- .../exception/UserAccountException.java | 88 +- .../exception/UserActionException.java | 86 +- .../ExpectedLinkedSessionException.java | 88 +- .../permanent/ProtocolFailureException.java | 88 +- ...ingPartyAccountConfigurationException.java | 92 +- .../permanent/ServerMaintenanceException.java | 84 +- .../permanent/SmartIdClientException.java | 108 +- .../SmartIdRequestSetupException.java | 108 +- .../permanent/SmartIdServerException.java | 84 +- .../CertificateLevelMismatchException.java | 102 +- .../DocumentUnusableException.java | 80 +- ...eAccountOfRequestedTypeFoundException.java | 92 +- ...ersonShouldViewSmartIdPortalException.java | 102 +- ...InteractionNotSupportedByAppException.java | 86 +- .../UserAccountNotFoundException.java | 84 +- .../UserAccountUnusableException.java | 84 +- .../useraction/SessionTimeoutException.java | 84 +- .../UserRefusedCertChoiceException.java | 82 +- ...erRefusedConfirmationMessageException.java | 82 +- ...essageWithVerificationChoiceException.java | 82 +- ...UserRefusedDisplayTextAndPinException.java | 82 +- .../useraction/UserRefusedException.java | 104 +- ...electedWrongVerificationCodeException.java | 86 +- .../ee/sk/smartid/rest/LoggingFilter.java | 316 +- .../sk/smartid/rest/SessionStatusPoller.java | 218 +- .../ee/sk/smartid/rest/SmartIdConnector.java | 394 +- .../sk/smartid/rest/SmartIdRestConnector.java | 820 ++-- .../AcspV2SignatureProtocolParameters.java | 82 +- .../CertificateByDocumentNumberRequest.java | 86 +- .../sk/smartid/rest/dao/CertificateInfo.java | 82 +- .../smartid/rest/dao/CertificateResponse.java | 82 +- ...eviceLinkAuthenticationSessionRequest.java | 112 +- ...ceLinkCertificateChoiceSessionRequest.java | 106 +- .../rest/dao/DeviceLinkSessionResponse.java | 142 +- .../DeviceLinkSignatureSessionRequest.java | 114 +- .../ee/sk/smartid/rest/dao/Interaction.java | 82 +- .../dao/LinkedSignatureSessionRequest.java | 114 +- .../dao/LinkedSignatureSessionResponse.java | 76 +- ...ificationAuthenticationSessionRequest.java | 112 +- ...ficationAuthenticationSessionResponse.java | 82 +- ...cationCertificateChoiceSessionRequest.java | 104 +- ...ationCertificateChoiceSessionResponse.java | 80 +- .../NotificationSignatureSessionRequest.java | 110 +- .../NotificationSignatureSessionResponse.java | 84 +- .../RawDigestSignatureProtocolParameters.java | 80 +- .../smartid/rest/dao/RequestProperties.java | 78 +- .../smartid/rest/dao/SemanticsIdentifier.java | 276 +- .../smartid/rest/dao/SessionCertificate.java | 160 +- .../rest/dao/SessionMaskGenAlgorithm.java | 160 +- .../SessionMaskGenAlgorithmParameters.java | 120 +- .../ee/sk/smartid/rest/dao/SessionResult.java | 202 +- .../rest/dao/SessionResultDetails.java | 124 +- .../sk/smartid/rest/dao/SessionSignature.java | 320 +- .../SessionSignatureAlgorithmParameters.java | 240 +- .../ee/sk/smartid/rest/dao/SessionStatus.java | 400 +- .../rest/dao/SessionStatusRequest.java | 210 +- .../dao/SignatureAlgorithmParameters.java | 76 +- .../sk/smartid/rest/dao/VerificationCode.java | 82 +- .../ee/sk/smartid/util/CallbackUrlUtil.java | 182 +- .../util/CertificateAttributeUtil.java | 406 +- .../ee/sk/smartid/util/InteractionUtil.java | 176 +- .../util/NationalIdentityNumberUtil.java | 284 +- src/main/java/ee/sk/smartid/util/SetUtil.java | 110 +- .../java/ee/sk/smartid/util/StringUtil.java | 132 +- .../trusted_certificates/EID-SK_2016.pem.crt | 78 +- .../trusted_certificates/NQ-SK_2016.pem.crt | 74 +- .../AuthenticationIdentityMapperTest.java | 108 +- .../smartid/AuthenticationIdentityTest.java | 104 +- .../AuthenticationResponseMapperImplTest.java | 1674 ++++---- .../smartid/CapabilitiesArgumentProvider.java | 100 +- ...ateByDocumentNumberRequestBuilderTest.java | 580 +-- ...ertificateChoiceResponseValidatorTest.java | 580 +-- .../ee/sk/smartid/CertificateParserTest.java | 82 +- .../java/ee/sk/smartid/CertificateUtil.java | 144 +- .../smartid/CertificateValidatorImplTest.java | 154 +- .../sk/smartid/ClientRequestHeaderFilter.java | 102 +- .../DefaultTrustedCAStoreBuilderTest.java | 136 +- ...nkAuthenticationResponseValidatorTest.java | 534 +-- ...thenticationSessionRequestBuilderTest.java | 962 ++--- .../ee/sk/smartid/DeviceLinkBuilderTest.java | 1100 ++--- ...ficateChoiceSessionRequestBuilderTest.java | 556 +-- ...inkSignatureSessionRequestBuilderTest.java | 1044 ++--- .../ee/sk/smartid/DigestCalculatorTest.java | 158 +- ...plicateDeviceLinkInteractionsProvider.java | 100 +- ...tificationInteractionArgumentProvider.java | 98 +- .../ee/sk/smartid/ErrorResultHandlerTest.java | 262 +- .../FileDefaultTrustedCAStoreBuilderTest.java | 204 +- src/test/java/ee/sk/smartid/FileUtil.java | 110 +- .../smartid/InvalidCertificateGenerator.java | 292 +- .../InvalidRpChallengeArgumentProvider.java | 100 +- ...ionSignatureSessionRequestBuilderTest.java | 516 +-- ...natureCertificatePurposeValidatorTest.java | 240 +- ...onAuthenticationResponseValidatorTest.java | 408 +- ...thenticationSessionRequestBuilderTest.java | 770 ++-- ...ficateChoiceSessionRequestBuilderTest.java | 538 +-- ...ionSignatureSessionRequestBuilderTest.java | 992 ++--- .../ee/sk/smartid/QrCodeGeneratorTest.java | 420 +- src/test/java/ee/sk/smartid/QrCodeUtil.java | 138 +- ...natureCertificatePurposeValidatorTest.java | 310 +- .../sk/smartid/RpChallengeGeneratorTest.java | 138 +- ...essionEndResultErrorArgumentsProvider.java | 130 +- .../java/ee/sk/smartid/SignableDataTest.java | 136 +- .../java/ee/sk/smartid/SignableHashTest.java | 128 +- .../SignatureResponseValidatorTest.java | 1174 +++--- .../SignatureValueValidatorImplTest.java | 242 +- .../java/ee/sk/smartid/SmartIdClientTest.java | 1622 ++++---- .../ee/sk/smartid/SmartIdDemoCondition.java | 104 +- .../smartid/SmartIdDemoIntegrationTest.java | 80 +- .../sk/smartid/SmartIdRestServiceStubs.java | 306 +- ...erRefusedInteractionArgumentsProvider.java | 96 +- .../VerificationCodeCalculatorTest.java | 166 +- ...cationCertificatePurposeValidatorTest.java | 334 +- ...cationCertificatePurposeValidatorTest.java | 340 +- .../common/InteractionValidatorTest.java | 146 +- .../common/InteractionsMapperTest.java | 176 +- .../devicelink/UrlSafeTokenGeneratorTest.java | 154 +- .../DeviceLinkInteractionTest.java | 200 +- .../NotificationInteractionTest.java | 250 +- .../smartid/dao/SemanticsIdentifierTest.java | 120 +- .../integration/ReadmeIntegrationTest.java | 1898 ++++----- .../SmartIdRestIntegrationTest.java | 664 +-- .../smartid/rest/SessionStatusPollerTest.java | 166 +- .../rest/SmartIdRestConnectorTest.java | 3558 ++++++++--------- .../sk/smartid/util/CallbackUrlUtilTest.java | 210 +- .../util/CertificateAttributeUtilTest.java | 284 +- .../util/NationalIdentityNumberUtilTest.java | 282 +- src/test/resources/logback.xml | 30 +- ...ation-session-request-invalid-request.json | 12 +- ...uthentication-session-request-qr-code.json | 26 +- ...ession-request-same-device-all-fields.json | 34 +- ...uest-same-device-only-required-fields.json | 28 +- ...entication-session-request-all-fields.json | 36 +- ...uthentication-session-request-invalid.json | 8 +- ...-session-request-only-required-fields.json | 26 +- ...by-document-number-request-all-fields.json | 8 +- ...t-number-request-only-required-fields.json | 6 +- ...ice-link-signature-request-all-fields.json | 26 +- ...device-link-signature-request-qr-code.json | 24 +- ...ce-link-signature-request-same-device.json | 26 +- ...ate-choice-session-request-all-fields.json | 18 +- ...te-choice-session-request-device-link.json | 10 +- ...te-choice-session-request-for-qr-code.json | 8 +- ...-signature-session-request-all-fields.json | 36 +- ...-session-request-only-required-fields.json | 26 +- ...ate-choice-session-request-all-fields.json | 16 +- ...e-session-request-invalid-credentials.json | 6 +- ...ficate-choice-session-request-invalid.json | 4 +- ...-session-request-only-required-fields.json | 6 +- ...-signature-session-request-all-fields.json | 34 +- ...e-session-request-invalid-credentials.json | 24 +- ...ion-signature-session-request-invalid.json | 4 +- ...-session-request-only-required-fields.json | 24 +- ...-link-authentication-session-response.json | 12 +- .../notification-session-response.json | 6 +- ...ocument-number-response-unknown-state.json | 18 +- ...rtificate-by-document-number-response.json | 16 +- .../session-status-account-unusable.json | 14 +- .../session-status-document-unusable.json | 14 +- ...ession-status-expected-linked-session.json | 14 +- .../session-status-protocol-failure.json | 14 +- ...tatus-running-with-ignored-properties.json | 8 +- .../responses/session-status-running.json | 6 +- .../session-status-server-error.json | 14 +- ...sion-status-successful-authentication.json | 74 +- ...-status-successful-certificate-choice.json | 28 +- .../session-status-successful-signature.json | 74 +- .../responses/session-status-timeout.json | 14 +- ...ssion-status-user-refused-cert-choice.json | 14 +- ...s-user-refused-confirmation-vc-choice.json | 22 +- ...sion-status-user-refused-confirmation.json | 22 +- ...tus-user-refused-display-text-and-pin.json | 22 +- ...session-status-user-refused-vc-choice.json | 22 +- .../session-status-user-refused.json | 14 +- .../responses/session-status-wrong-vc.json | 14 +- ...evice-link-signature-session-response.json | 12 +- ...k-certificate-choice-session-response.json | 12 +- ...tification-signature-session-response.json | 6 +- ...n-certificate-choice-session-response.json | 6 +- ...tification-signature-session-response.json | 12 +- src/test/resources/sid_demo_sk_ee.pem | 78 +- .../test-certs/TEST_SK_ROOT_G1_2021E.pem.crt | 34 +- .../TEST_of_SK_OCSP_RESPONDER_2020.pem.cer | 56 +- .../auth-cert-40504040001-demo-q.crt | 76 +- .../test-certs/auth-cert-40504040001.pem.crt | 76 +- .../auth-pnolv-020100-29990-mock-q.crt | 92 +- src/test/resources/test-certs/ca-cert.pem.crt | 46 +- .../cert-choice-cert-40504040001.pem.cert | 84 +- .../resources/test-certs/expired-cert.pem.crt | 78 +- .../test-certs/nq-auth-cert-40504049999.crt | 76 +- .../resources/test-certs/nq-signing-cert.pem | 74 +- .../test-certs/other-auth-cert.pem.crt | 78 +- .../test-certs/sign-cert-40504040001.pem.crt | 84 +- .../TEST_EID-NQ_2021E.pem.crt | 44 +- ...EST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt | 46 +- 291 files changed, 32030 insertions(+), 32030 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index a31c170b..9e1f8b92 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -1,40 +1,40 @@ -name: Run dependency and spotbugs checks - -on: - workflow_run: - workflows: ["Run tests"] - types: - - completed - workflow_dispatch: - -permissions: - contents: read - -jobs: - run-checks: - name: Run dependency and spotbugs checks - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Run dependency check - run: | - ./mvnw -DossIndexUsername=${{ secrets.ossIndexUsername }} -DossIndexPassword=${{ secrets.ossIndexPassword }} -DnvdApiKey=${{ secrets.nvdApiKey }} org.owasp:dependency-check-maven:check - - - name: Archive dependency report - uses: actions/upload-artifact@v4 - with: - name: dependency-report - path: target/dependency-check-report.html - - - name: Run spotbugs check - run: | - ./mvnw spotbugs:check +name: Run dependency and spotbugs checks + +on: + workflow_run: + workflows: ["Run tests"] + types: + - completed + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-checks: + name: Run dependency and spotbugs checks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Run dependency check + run: | + ./mvnw -DossIndexUsername=${{ secrets.ossIndexUsername }} -DossIndexPassword=${{ secrets.ossIndexPassword }} -DnvdApiKey=${{ secrets.nvdApiKey }} org.owasp:dependency-check-maven:check + + - name: Archive dependency report + uses: actions/upload-artifact@v4 + with: + name: dependency-report + path: target/dependency-check-report.html + + - name: Run spotbugs check + run: | + ./mvnw spotbugs:check diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index b138f84d..703134e3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,64 +1,64 @@ -name: Publish to maven repository - -on: - release: - types: - - published - -permissions: - contents: read - -jobs: - package_and_publish: - name: Publish to maven repository - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup java SDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - #passphrase: ${{ secrets.PASSPHRASE }} - - - name: Create bundle and upload to oss.sonatype.org (staging) - # Fail on first error - run: | - set -e - version=${{ github.event.release.name }} - artifact=smart-id-java-client-$version - echo "[INFO] Artifact name: $artifact" - ./mvnw versions:set -DnewVersion="$version" - ./mvnw package -DskipTests - cd target - rm -rf ee/sk/smartid/smart-id-java-client/$version - mkdir -p ee/sk/smartid/smart-id-java-client/$version - cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ - cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ - cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ - cp ../pom.xml ee/sk/smartid/smart-id-java-client/$version/$artifact.pom - cd ee/sk/smartid/smart-id-java-client/$version - gpg -ab $artifact.pom - gpg -ab $artifact.jar - gpg -ab $artifact-sources.jar - gpg -ab $artifact-javadoc.jar - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha256sum "$file" | cut -d " " -f 1 > "$file.sha256"; done' _ {} + - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha1sum "$file" | cut -d " " -f 1 > "$file.sha1"; done' _ {} + - find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do md5sum "$file" | cut -d " " -f 1 > "$file.md5"; done' _ {} + - cd ../../../../../ - zip bundle.zip ee/sk/smartid/smart-id-java-client/$version/* - CODE=$(curl -w "%{http_code}" -o curl_response.txt -s --request POST --verbose --header 'Authorization: Bearer ${{ secrets.SONATYPETOKEN }}' --form bundle=@bundle.zip https://central.sonatype.com/api/v1/publisher/upload) - echo "[INFO] ------------------------------------------------------------------------" - echo "[INFO] Upload to central.sonatype.com ResponseCode: $CODE" - cat curl_response.txt - echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" - echo "[INFO] ------------------------------------------------------------------------" - [[ $CODE == 201 ]] && exit 0 || exit 1 - +name: Publish to maven repository + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + package_and_publish: + name: Publish to maven repository + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup java SDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - + name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + #passphrase: ${{ secrets.PASSPHRASE }} + + - name: Create bundle and upload to oss.sonatype.org (staging) + # Fail on first error + run: | + set -e + version=${{ github.event.release.name }} + artifact=smart-id-java-client-$version + echo "[INFO] Artifact name: $artifact" + ./mvnw versions:set -DnewVersion="$version" + ./mvnw package -DskipTests + cd target + rm -rf ee/sk/smartid/smart-id-java-client/$version + mkdir -p ee/sk/smartid/smart-id-java-client/$version + cp $artifact.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-sources.jar ee/sk/smartid/smart-id-java-client/$version/ + cp $artifact-javadoc.jar ee/sk/smartid/smart-id-java-client/$version/ + cp ../pom.xml ee/sk/smartid/smart-id-java-client/$version/$artifact.pom + cd ee/sk/smartid/smart-id-java-client/$version + gpg -ab $artifact.pom + gpg -ab $artifact.jar + gpg -ab $artifact-sources.jar + gpg -ab $artifact-javadoc.jar + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha256sum "$file" | cut -d " " -f 1 > "$file.sha256"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do sha1sum "$file" | cut -d " " -f 1 > "$file.sha1"; done' _ {} + + find . -type f \( -name '*.jar' -o -name '*.pom' \) -exec sh -c 'for file; do md5sum "$file" | cut -d " " -f 1 > "$file.md5"; done' _ {} + + cd ../../../../../ + zip bundle.zip ee/sk/smartid/smart-id-java-client/$version/* + CODE=$(curl -w "%{http_code}" -o curl_response.txt -s --request POST --verbose --header 'Authorization: Bearer ${{ secrets.SONATYPETOKEN }}' --form bundle=@bundle.zip https://central.sonatype.com/api/v1/publisher/upload) + echo "[INFO] ------------------------------------------------------------------------" + echo "[INFO] Upload to central.sonatype.com ResponseCode: $CODE" + cat curl_response.txt + echo -e "\n[INFO] Login to central.sonatype.com for releasing $artifact" + echo "[INFO] ------------------------------------------------------------------------" + [[ $CODE == 201 ]] && exit 0 || exit 1 + diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index bfd4fd91..f0598fd2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,37 +1,37 @@ -name: Run tests - -on: - push: - branches: [ "master", "v3.1" ] - pull_request: - branches: [ "master" ] - -permissions: - contents: read - -jobs: - run-tests: - runs-on: ubuntu-latest - strategy: - matrix: - java-version: ['17', '21'] - name: Run tests with java SDK ${{ matrix.java-version }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup java - uses: actions/setup-java@v4 - with: - java-version: ${{ matrix.java-version }} - distribution: 'temurin' - cache: maven - - - name: Check JAVA version (v${{ matrix.java-version }}) - run: java -version - - - name: Run tests - # Fail on first error - run: | - set -e +name: Run tests + +on: + push: + branches: [ "master", "v3.1" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: ['17', '21'] + name: Run tests with java SDK ${{ matrix.java-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + + - name: Check JAVA version (v${{ matrix.java-version }}) + run: java -version + + - name: Run tests + # Fail on first error + run: | + set -e mvn test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39b4f2c9..91fb933f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -.idea -*.iml -target -.settings -.classpath -.project +.idea +*.iml +target +.settings +.classpath +.project diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java index 519f798c..b901097f 100644 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -1,117 +1,117 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 0169a252..8c79a83a 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,18 +1,18 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/CHANGELOG.md b/CHANGELOG.md index 880c2a63..31e9571b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,325 +1,325 @@ -# Changelog - -All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## [3.1-?] - TBD - -### Structural changes - -- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. -- Removed all Smart-ID v2 related classes, tests, and documentation. -- Updated README to reflect removal of v2-related information. - -### Dynamic-link auth to device-link auth changes - -- Renamed dynamic-link authentication to device-link authentication. -- Updated authentication endpoints to use /device-link/ paths. -- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). -- Replaced signature algorithm list with fixed `rsassa-pss`. -- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. -- Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. -- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. -- Introduced a `DeviceLinkBuilder` to generate device links. - - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. - - Ensures `elapsedSeconds` is only used for QR_CODE flows. - - Moved `deviceLinkBase` to required input (no more default). - - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. - - New payload structure includes required and optional fields as per documentation. - - `schemeName` is now configurable (default is `"smart-id"`). - - Does not store `sessionSecret`, ensures it must be passed to the build method. -- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. - -- Updates to session status response - - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. - - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling - - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` - - Renamed `interactionFlowUsed` to `interactionTypeUsed`. -- Updated exception message of `DocumentUnusableException` -- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response -- Updated AuthenticationSessionRequest and related classes to records. -- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. - - Created to builder-classes for loading trusted CA certificates - - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore - - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates -- Update AuthenticationResponseValidator to DeviceLinkAuthenticationResponseValidator - - update signature value validation - - added additional certificate validations (validate certificate chain and certificate purpose) - - added validation for userChallenge and userChallengeVerifier in case of same device flows - - added validators QualifiedAuthenticationCertificatePurposeValidator and NonQualifiedAuthenticationCertificatePurposeValidator to validate - certificate purpose based on requested certificate level. - -- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest - -### Added handling for querying certificate by document number - -- Added new endpoint: `POST /v3/signature/certificate/{document-number}`. -- Added new builder CertificateByDocumentNumberRequestBuilder to create the request -- Add new request objects CertificateByDocumentNumberRequest and response CertificateResponse -- Removed notification-based certificate choice request with document number. - -### Updated dynamic-link signature to device-link signature - -- Renamed dynamic-link signature to device-link signature. -- Updated signature endpoints to use /device-link/ paths. -- Replaced signature algorithm list with fixed `rsassa-pss`. -- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. -- Converted interaction list to Base64 string and ensured no duplicates. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Removed HashType and update SignableHash and SignableData to use HashAlgorithm -- Update signature session-status validations - - Signature - - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. - - Allowed `flowType`: QR · App2App · Web2App · Notification. - - Fixed `signatureAlgorithm` to `rsassa-pss`. - - `signatureAlgorithmParameters` - - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. - - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. - - `saltLength`: 32 / 48 / 64 bytes to match chosen hash algorithm octet length. - - `trailerField`: `0xbc`. - - - Certificate - - Must be a Smart-ID *signature* certificate: - - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or - `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. - - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. - - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. - -- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and - `SignatureResponseValidator`. - -## Update dynamic-link certificate choice to device-link certificate choice - -- Renamed dynamic-link certificate choice to device-link certificate choice. -- Updated certificate choice endpoint to use /device-link/ paths. -- Added `initialCallbackUrl` field with regex validation. -- Added `deviceLinkBase` to session response. -- Updated CertificateChoiceResponseMapper - - Renamed to CertificateChoiceResponseValidator - - Added CertificateValidator as dependency - -## Added linked signature session support - -- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. -- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. -- Added request LinkedSignatureSessionRequest and LinkedSignatureSessionResponse. - -### Updated notification-based authentication to work with Smart-ID API v3.1 - -- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 -- Removed verificationCodeChoice interactions and related handling -- Removed AuthenticationHash. -- Added NotificationAuthenticationResponseValidator - -### Updated notification-based certificate choice to work with Smart-ID API v3.1 - -- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint -- Added NotificationCertificateChoiceSessionRequest - -### Updated notification-based signature to work with Smart-ID API v3.1 - -- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint -- Added NotificationSignatureSessionRequest - -## [3.0] - 2023-10-14 - -### Added -- Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 - package. - - New builder classes to start v3 sessions: - - DynamicLinkAuthenticationSessionRequestBuilder - - DynamicLinkCertificateChoiceSessionRequestBuilder - - DynamicLinkSignatureSessionRequestBuilder - - NotificationAuthenticationSessionRequestBuilder - - NotificationCertificateChoiceSessionRequestBuilder - - NotificationSignatureSessionRequestBuilder - - Helper class for dynamic link - - AuthCode - used for generating authCode necessary for dynamic-link - - QrCodeGenerator - to create QR-code from dynamic-link - - DynamicContentBuilder - to create dynamic link or QR-code - - Support for sessions status request handling for the v3 path. - - Added AuthenticationResponseMapper for validating required fields and mapping session status to authentication response - - Added AuthenticationResponseValidator to validate certificate and signed authentication response and construct AuthenticationIdentity - - Added SignatureResponseMapper for validating required fields and mapping session status to signature response - - Added CertificateChoiceResponseMapper for validating required fields and mapping session status to certificate choice response - -### Changed -- Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. -- Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` -- Typo fixes, code cleanup and improvements -- Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. - -### Removed -- Removed deprecated methods from AuthenticationIdentity - -### Java and dependency updates -- Updated minimal supported java to version 17 -- Updated slf4j-api to version 2.0.16 -- Updated jackson dependencies to version 2.17.2 -- Added jakarta.ws.rs:jakarta.ws.rs-api -- Updated jersey dependencies to version 3.1.8 -- Updated bouncy-castle artifact to bcprov-jdk18on on version 1.78.1 -- Updated jaxb-runtime to version 4.0.5 - -## [2.3] - 2023-05-06 -- To request the IP address of the device running Smart-ID app, the following methods were added: - - AuthenticationRequestBuilder.withShareMdClientIpAddress(boolean) - - CertificateRequestBuilder.withShareMdClientIpAddress(boolean) - - SignatureRequestBuilder.withShareMdClientIpAddress(boolean) -- The IP address returned can be read out using: - - SmartIdAuthenticationResponse.getDeviceIpAddress() - - SmartIdCertificate.getDeviceIpAddress() - - SmartIdSignature.getDeviceIpAddress() - -## [2.2.2] - 2022-11-14 - -### Changed -- upgrade jackson, jersey and dependency-check-maven plugin -### Documented -- How to extract date-of-birth from a certificate added as a separate paragraph to readme. -- Added two tests into SmartIdIntegrationTest that demonstrate fetching and parsing a certificate with date-of-birth -- Changed demo SSL certificate -- add correct way of adding trusted certificates in Readme [#73](https://github.com/SK-EID/smart-id-java-client/issues/73) - -## [2.2.1] - 2022-09-12 - -### Fixed -- added jakarta.ws.rs:jakarta.ws.rs-api as a dependency to avoid ClassNotFoundException with spring framework - -### Changed -- Updated dependencies - -### Changes in tests and documentation -- How to use a proxy server - added documentation to README.md and tests to ReadmeTest.java - -## [2.2] - 2022-02-22 - -### Changed -- Reduced number of external dependencies by removing commons-lang3, commons-io, commons-codec. - -### Added -- [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java#:~:text=getDeviceIpAddress()) -- [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java#:~:text=getDeviceIpAddress()) -- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) - -## [2.1.4] - 2022-01-14 - -### Fixed -- bug where non-Baltic certificates without date-of-birth resulted with an exception - -## [2.1.3] - 2021-12-22 - -### Fixed -- Possible NPE fix (in rare cases under load testing the SessionStatus is null) - -### Changes in tests -- Changed document number in tests -- Added a flag (SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO) to switch off tests that make requests to Smart-ID demo env. - -## [2.1.2] - 2021-11-03 - -### Changed -- AuthenticationResponseValidator.constructAuthenticationIdentity() converted into a static method - -## [2.1.1] - 2021-09-06 - -### Fixed -- Bug fixed in parsing date of birth for Latvian ID-codes. - -## [2.1] - 2021-07-07 - -### Added -- AuthenticationIdentity.getDateOfBirth() to get person birthdate (if available). -- Add library version number and Java major release number to User-Agent header of outgoing requests - -## [2.0] - 2020-11-20 - -### Changed -- Switch to Smart-ID API 2.0 -- `AuthenticationResponseValidator.validate()` returns AuthenticationIdentity if validation passes. - If validation fails then `SmartIdResponseValidationException` or its subclass `CertificateLevelMismatchException` (if signer's certificate is below requested level) is thrown. -- Grouped exceptions thrown by library to reduce need to handle each exception individually. See Readme.md for detail info. -- Minimum Java level raised to Java 8 -- Relying Party must keep a list of trusted certificates (in plain text or in a trust store). -- request.setVcChoice() was removed in Smart-ID API 2.0 and replaced by request.setAllowedInteractionsOrder(); - - -### Added -- New parameter `allowedInteractionsOrder` added to authentication and signing requests. It replaces parameters displayText and requestProperties.vcChoice -- New parameter `interactionFlowUsed` added into session status response message. -- If user refuses then a dedicated exception is thrown that indicates exact screen where user pressed cancel. Thrown exception is subclass of `UserRefusedException`. - -### Removed -- all endpoints using `NationalIdentityNumber` are now removed as this functionality has been removed from Smart-ID API 2.0 -- errors that the caller cannot recover from are now removed from method throws list. -- Hard-coded certificates were removed together with methods: - - SmartIdClient.useDemoEnvSSLCertificates() - - SmartIdClient.useLiveEnvSSLCertificates() - -## [1.6] - 2020-05-25 - -### Added -- UserSelectedWrongVerificationCodeException is now thrown when user selects wrong verification code from three-choice selection. - -## [1.5.1] - 2020-05-18 -### Security -- Bumped jackson-databind from 2.9.10.1 to 2.9.10.4 -- Updated Maven Dependency Check plugin version. - -### Changed -- AuthenticationRequestBuilder method withRequestProperties access modifier changed to public - -### Added - -- Maven wrapper to project - -## [1.5] - 2019-11-12 -### Security -- CVE-2019-16943 -- CVE-2019-17531 -- CVE-2019-16942 -- CVE-2019-16335 -- CVE-2019-14540 -### Added -- SSL pinning to verify, that the client is communicating with SK environment [#3](https://github.com/SK-EID/smart-id-java-client/issues/3) -- SmartIdClient.addTrustedSSLCertificates(String ...sslCertificate) - add ssl certificates when Sk starts to use new certs -- SmartIdClient.setTrustedSSLCertificates(String ...sslCertificates) - set specific ssl certificates to trust -- SmartIdClient.useDemoEnvSSLCertificates() - uses only demo env ssl certificates -- SmartIdClient.useLiveEnvSSLCertificates() - uses only live env ssl certificates -- SmartIdClient.loadSslCertificatesFromKeystore(KeyStore keyStore) - loads only the certificates from keystore - -## [1.4] - 2019-09-23 -### Added -- Client configuration on different JAX-WS implementations. [#22](https://github.com/SK-EID/smart-id-java-client/issues/22), [#11](https://github.com/SK-EID/mid-rest-java-client/issues/11) -- SmartIdClient.setConfiguredClient() -- SmartIdClient.setNetworkConnectionConfig() - -## [1.3] - 2019-09-13 -### Added -- Capabilities parameter ([#25](https://github.com/SK-EID/smart-id-java-client/pull/25)) -- [Request properties](https://github.com/SK-EID/smart-id-documentation#416-request-properties) (vcChoice) for authentication and signing ([#21](https://github.com/SK-EID/smart-id-java-client/pull/21)) - -## [1.2] - 2019-08-21 -### Added -- Support for [Semantics Identifier](https://github.com/SK-EID/smart-id-documentation#412-rest-object-references) ([#17](https://github.com/SK-EID/smart-id-java-client/pull/17)) -- Document number to authentication responses ([#14](https://github.com/SK-EID/smart-id-java-client/issues/14)) -- Maven dependency check plugin for continuous security -- SpotBugs plugin for continuous bug detection - -## [1.1] - 2018-12-10 - -### Added -- SmartIdClient.getSmartIdConnector() -- SmartIdRequestBuilder.validateSessionResult -- MIT license to code base - -### Changed -- renamed SignatureSessionResponse.sessionId -> SignatureSessionResponse.sessionID -- renamed SmartIdRestConnector -> SmartIdConnector -- renamed SessionStatus.getCertificate() -> SessionStatus.getCert() -- renamed SessionSignature.getValueInBase64() -> SessionSignature.getValue() -- improved and cleaned up tests +# Changelog + +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [3.1-?] - TBD + +### Structural changes + +- Moved Smart-ID v3 related classes from ee.sk.smartid.v3 package to root ee.sk.smartid package. +- Removed all Smart-ID v2 related classes, tests, and documentation. +- Updated README to reflect removal of v2-related information. + +### Dynamic-link auth to device-link auth changes + +- Renamed dynamic-link authentication to device-link authentication. +- Updated authentication endpoints to use /device-link/ paths. +- Replaced `randomChallenge` with `rpChallenge` (Base64, length 44–88). +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Added new exception `SmartIdRequestSetupException` to handle cases when invalid values are provided for building session request objects. +- Replaced old dynamic content and authCode generation logic to match Smart-ID v3.1 authCode specification. +- Introduced a `DeviceLinkBuilder` to generate device links. + - Validates required parameters such as `deviceLinkBase`, `version`, `deviceLinkType`, `sessionType`, `lang`, `elapsedSeconds` and `sessionToken`. + - Ensures `elapsedSeconds` is only used for QR_CODE flows. + - Moved `deviceLinkBase` to required input (no more default). + - Handles both unprotected device-link generation and HMAC-SHA256 based authCode calculation as per specification. + - New payload structure includes required and optional fields as per documentation. + - `schemeName` is now configurable (default is `"smart-id"`). + - Does not store `sessionSecret`, ensures it must be passed to the build method. +- Removed deprecated dynamic link and QR code generation logic from old builders and helpers. + +- Updates to session status response + - Updated USER_REFUSED_INTERACTION responses and updated error handling for these cases. + - Added new `endResult` error responses (`PROTOCOL_FAILURE`, `EXPECTED_LINKED_SESSION`, `SERVER_ERROR`) with handling + - Added new fields: `userChallenge`, `flowType`, `signatureAlgorithmParameters` + - Renamed `interactionFlowUsed` to `interactionTypeUsed`. +- Updated exception message of `DocumentUnusableException` +- Added AccountUnusableException to handle ACCOUNT_UNUSABLE endResult from session status response +- Updated AuthenticationSessionRequest and related classes to records. +- Refactored loading of trusted CA certificates from AuthenticationResponseValidator to their own class `DefaultTrustedCACertStore`. + - Created to builder-classes for loading trusted CA certificates + - `FileTrustedCACertStoreBuilder` for loading trust anchors and intermediate CA certificates from truststore + - `DefaultTrustedCACertStoreBuilder` for creating DefaultTrustedCACertStore with preloaded certificates, also validates provided certificates +- Update AuthenticationResponseValidator to DeviceLinkAuthenticationResponseValidator + - update signature value validation + - added additional certificate validations (validate certificate chain and certificate purpose) + - added validation for userChallenge and userChallengeVerifier in case of same device flows + - added validators QualifiedAuthenticationCertificatePurposeValidator and NonQualifiedAuthenticationCertificatePurposeValidator to validate + certificate purpose based on requested certificate level. + +- Added CallbackUrlUtil to generate callback URL with token and provides method to validate sessionSecretDigest + +### Added handling for querying certificate by document number + +- Added new endpoint: `POST /v3/signature/certificate/{document-number}`. +- Added new builder CertificateByDocumentNumberRequestBuilder to create the request +- Add new request objects CertificateByDocumentNumberRequest and response CertificateResponse +- Removed notification-based certificate choice request with document number. + +### Updated dynamic-link signature to device-link signature + +- Renamed dynamic-link signature to device-link signature. +- Updated signature endpoints to use /device-link/ paths. +- Replaced signature algorithm list with fixed `rsassa-pss`. +- Added required `signatureAlgorithmParameters.hashAlgorithm` field with validation. +- Converted interaction list to Base64 string and ensured no duplicates. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Removed HashType and update SignableHash and SignableData to use HashAlgorithm +- Update signature session-status validations + - Signature + - `signature.value` must match `^[A-Za-z0-9+/]+={0,2}$`. + - Allowed `flowType`: QR · App2App · Web2App · Notification. + - Fixed `signatureAlgorithm` to `rsassa-pss`. + - `signatureAlgorithmParameters` + - `hashAlgorithm`: `SHA-256/384/512, SHA3-256/384/512`. + - `maskGenAlgorithm.algorithm`: `id-mgf1` & its `hashAlgorithm` must equal the main hash. + - `saltLength`: 32 / 48 / 64 bytes to match chosen hash algorithm octet length. + - `trailerField`: `0xbc`. + + - Certificate + - Must be a Smart-ID *signature* certificate: + - `CertificatePolicies (2.5.29.32)` contain either `qualified``1.3.6.1.4.1.10015.17.2`, `0.4.0.194112.1.2`or + `non-qualified``1.3.6.1.4.1.10015.17.1`, `0.4.0.2042.1.1`. + - `KeyUsage (2.5.29.15)` – NonRepudiation bit set. + - `QC-Statement (1.3.6.1.5.5.7.1.3)` contains `0.4.0.1862.1.6.1`. + +- Extracted common certificate validation logic into `CertificateValidator` and will be used by `AuthenticationResponseValidator` and + `SignatureResponseValidator`. + +## Update dynamic-link certificate choice to device-link certificate choice + +- Renamed dynamic-link certificate choice to device-link certificate choice. +- Updated certificate choice endpoint to use /device-link/ paths. +- Added `initialCallbackUrl` field with regex validation. +- Added `deviceLinkBase` to session response. +- Updated CertificateChoiceResponseMapper + - Renamed to CertificateChoiceResponseValidator + - Added CertificateValidator as dependency + +## Added linked signature session support + +- Added endpoint for creating linked signature session `POST /v3/signature/notification/linked/{document-number}`. +- Added builder to create linked signature session request `LinkedSignatureSessionRequestBuilder`. +- Added request LinkedSignatureSessionRequest and LinkedSignatureSessionResponse. + +### Updated notification-based authentication to work with Smart-ID API v3.1 + +- Updated notification-based authentication session request creation to be usable with Smart-ID API v3.1 +- Removed verificationCodeChoice interactions and related handling +- Removed AuthenticationHash. +- Added NotificationAuthenticationResponseValidator + +### Updated notification-based certificate choice to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based certificate choice endpoint +- Added NotificationCertificateChoiceSessionRequest + +### Updated notification-based signature to work with Smart-ID API v3.1 + +- Updated SmartIdRestConnector to use v3.1 notification-based signature endpoint +- Added NotificationSignatureSessionRequest + +## [3.0] - 2023-10-14 + +### Added +- Support for handling RP API v3.0 requests. View V3 section in README.md for more information. Related classes can be found in the ee.sk.smartid.v3 + package. + - New builder classes to start v3 sessions: + - DynamicLinkAuthenticationSessionRequestBuilder + - DynamicLinkCertificateChoiceSessionRequestBuilder + - DynamicLinkSignatureSessionRequestBuilder + - NotificationAuthenticationSessionRequestBuilder + - NotificationCertificateChoiceSessionRequestBuilder + - NotificationSignatureSessionRequestBuilder + - Helper class for dynamic link + - AuthCode - used for generating authCode necessary for dynamic-link + - QrCodeGenerator - to create QR-code from dynamic-link + - DynamicContentBuilder - to create dynamic link or QR-code + - Support for sessions status request handling for the v3 path. + - Added AuthenticationResponseMapper for validating required fields and mapping session status to authentication response + - Added AuthenticationResponseValidator to validate certificate and signed authentication response and construct AuthenticationIdentity + - Added SignatureResponseMapper for validating required fields and mapping session status to signature response + - Added CertificateChoiceResponseMapper for validating required fields and mapping session status to certificate choice response + +### Changed +- Most of the existing code for RP API v2.0 has been moved into the ee.sk.smartid.v2 package for clarity. +- Replaced deprecated `X509Certificate::getSubjectDN()` with `X509Certificate::getSubjectX500Principal()` +- Typo fixes, code cleanup and improvements +- Modified NationalIdentityNumberUtil to handle LV person codes with prefixes 33-39 without throwing an exception during parsing. + +### Removed +- Removed deprecated methods from AuthenticationIdentity + +### Java and dependency updates +- Updated minimal supported java to version 17 +- Updated slf4j-api to version 2.0.16 +- Updated jackson dependencies to version 2.17.2 +- Added jakarta.ws.rs:jakarta.ws.rs-api +- Updated jersey dependencies to version 3.1.8 +- Updated bouncy-castle artifact to bcprov-jdk18on on version 1.78.1 +- Updated jaxb-runtime to version 4.0.5 + +## [2.3] - 2023-05-06 +- To request the IP address of the device running Smart-ID app, the following methods were added: + - AuthenticationRequestBuilder.withShareMdClientIpAddress(boolean) + - CertificateRequestBuilder.withShareMdClientIpAddress(boolean) + - SignatureRequestBuilder.withShareMdClientIpAddress(boolean) +- The IP address returned can be read out using: + - SmartIdAuthenticationResponse.getDeviceIpAddress() + - SmartIdCertificate.getDeviceIpAddress() + - SmartIdSignature.getDeviceIpAddress() + +## [2.2.2] - 2022-11-14 + +### Changed +- upgrade jackson, jersey and dependency-check-maven plugin +### Documented +- How to extract date-of-birth from a certificate added as a separate paragraph to readme. +- Added two tests into SmartIdIntegrationTest that demonstrate fetching and parsing a certificate with date-of-birth +- Changed demo SSL certificate +- add correct way of adding trusted certificates in Readme [#73](https://github.com/SK-EID/smart-id-java-client/issues/73) + +## [2.2.1] - 2022-09-12 + +### Fixed +- added jakarta.ws.rs:jakarta.ws.rs-api as a dependency to avoid ClassNotFoundException with spring framework + +### Changed +- Updated dependencies + +### Changes in tests and documentation +- How to use a proxy server - added documentation to README.md and tests to ReadmeTest.java + +## [2.2] - 2022-02-22 + +### Changed +- Reduced number of external dependencies by removing commons-lang3, commons-io, commons-codec. + +### Added +- [SmartIdAuthenticationResponse.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdAuthenticationResponse.java#:~:text=getDeviceIpAddress()) +- [SmartIdSignature.getDeviceIpAddress()](src/main/java/ee/sk/smartid/SmartIdSignature.java#:~:text=getDeviceIpAddress()) +- [SessionStatus.getDeviceIpAddress()](src/main/java/ee/sk/smartid/v2/rest/dao/SessionStatus.java#:~:text=getDeviceIpAddress()) + +## [2.1.4] - 2022-01-14 + +### Fixed +- bug where non-Baltic certificates without date-of-birth resulted with an exception + +## [2.1.3] - 2021-12-22 + +### Fixed +- Possible NPE fix (in rare cases under load testing the SessionStatus is null) + +### Changes in tests +- Changed document number in tests +- Added a flag (SmartIdIntegrationTest.TEST_AGAINST_SMART_ID_DEMO) to switch off tests that make requests to Smart-ID demo env. + +## [2.1.2] - 2021-11-03 + +### Changed +- AuthenticationResponseValidator.constructAuthenticationIdentity() converted into a static method + +## [2.1.1] - 2021-09-06 + +### Fixed +- Bug fixed in parsing date of birth for Latvian ID-codes. + +## [2.1] - 2021-07-07 + +### Added +- AuthenticationIdentity.getDateOfBirth() to get person birthdate (if available). +- Add library version number and Java major release number to User-Agent header of outgoing requests + +## [2.0] - 2020-11-20 + +### Changed +- Switch to Smart-ID API 2.0 +- `AuthenticationResponseValidator.validate()` returns AuthenticationIdentity if validation passes. + If validation fails then `SmartIdResponseValidationException` or its subclass `CertificateLevelMismatchException` (if signer's certificate is below requested level) is thrown. +- Grouped exceptions thrown by library to reduce need to handle each exception individually. See Readme.md for detail info. +- Minimum Java level raised to Java 8 +- Relying Party must keep a list of trusted certificates (in plain text or in a trust store). +- request.setVcChoice() was removed in Smart-ID API 2.0 and replaced by request.setAllowedInteractionsOrder(); + + +### Added +- New parameter `allowedInteractionsOrder` added to authentication and signing requests. It replaces parameters displayText and requestProperties.vcChoice +- New parameter `interactionFlowUsed` added into session status response message. +- If user refuses then a dedicated exception is thrown that indicates exact screen where user pressed cancel. Thrown exception is subclass of `UserRefusedException`. + +### Removed +- all endpoints using `NationalIdentityNumber` are now removed as this functionality has been removed from Smart-ID API 2.0 +- errors that the caller cannot recover from are now removed from method throws list. +- Hard-coded certificates were removed together with methods: + - SmartIdClient.useDemoEnvSSLCertificates() + - SmartIdClient.useLiveEnvSSLCertificates() + +## [1.6] - 2020-05-25 + +### Added +- UserSelectedWrongVerificationCodeException is now thrown when user selects wrong verification code from three-choice selection. + +## [1.5.1] - 2020-05-18 +### Security +- Bumped jackson-databind from 2.9.10.1 to 2.9.10.4 +- Updated Maven Dependency Check plugin version. + +### Changed +- AuthenticationRequestBuilder method withRequestProperties access modifier changed to public + +### Added + +- Maven wrapper to project + +## [1.5] - 2019-11-12 +### Security +- CVE-2019-16943 +- CVE-2019-17531 +- CVE-2019-16942 +- CVE-2019-16335 +- CVE-2019-14540 +### Added +- SSL pinning to verify, that the client is communicating with SK environment [#3](https://github.com/SK-EID/smart-id-java-client/issues/3) +- SmartIdClient.addTrustedSSLCertificates(String ...sslCertificate) - add ssl certificates when Sk starts to use new certs +- SmartIdClient.setTrustedSSLCertificates(String ...sslCertificates) - set specific ssl certificates to trust +- SmartIdClient.useDemoEnvSSLCertificates() - uses only demo env ssl certificates +- SmartIdClient.useLiveEnvSSLCertificates() - uses only live env ssl certificates +- SmartIdClient.loadSslCertificatesFromKeystore(KeyStore keyStore) - loads only the certificates from keystore + +## [1.4] - 2019-09-23 +### Added +- Client configuration on different JAX-WS implementations. [#22](https://github.com/SK-EID/smart-id-java-client/issues/22), [#11](https://github.com/SK-EID/mid-rest-java-client/issues/11) +- SmartIdClient.setConfiguredClient() +- SmartIdClient.setNetworkConnectionConfig() + +## [1.3] - 2019-09-13 +### Added +- Capabilities parameter ([#25](https://github.com/SK-EID/smart-id-java-client/pull/25)) +- [Request properties](https://github.com/SK-EID/smart-id-documentation#416-request-properties) (vcChoice) for authentication and signing ([#21](https://github.com/SK-EID/smart-id-java-client/pull/21)) + +## [1.2] - 2019-08-21 +### Added +- Support for [Semantics Identifier](https://github.com/SK-EID/smart-id-documentation#412-rest-object-references) ([#17](https://github.com/SK-EID/smart-id-java-client/pull/17)) +- Document number to authentication responses ([#14](https://github.com/SK-EID/smart-id-java-client/issues/14)) +- Maven dependency check plugin for continuous security +- SpotBugs plugin for continuous bug detection + +## [1.1] - 2018-12-10 + +### Added +- SmartIdClient.getSmartIdConnector() +- SmartIdRequestBuilder.validateSessionResult +- MIT license to code base + +### Changed +- renamed SignatureSessionResponse.sessionId -> SignatureSessionResponse.sessionID +- renamed SmartIdRestConnector -> SmartIdConnector +- renamed SessionStatus.getCertificate() -> SessionStatus.getCert() +- renamed SessionSignature.getValueInBase64() -> SessionSignature.getValue() +- improved and cleaned up tests diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 7ff3a10f..b7b18c47 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -1,22 +1,22 @@ -# Guidelines for developers of this library - -## Main design principles - -### Use as few external dependencies as possible - -Always prefer the functionality built into Java in favor of Apache Commons/Lang/Codec etc. - -You can check the effective dependency tree: -mvn dependency:tree -Dscope=compile - -### Add a license header for each file - -mvn license:update-file-header - -### Keep 3rd party licenses file updated - -If you add or remove dependencies then don't forget to run - -mvn license:add-third-party - +# Guidelines for developers of this library + +## Main design principles + +### Use as few external dependencies as possible + +Always prefer the functionality built into Java in favor of Apache Commons/Lang/Codec etc. + +You can check the effective dependency tree: +mvn dependency:tree -Dscope=compile + +### Add a license header for each file + +mvn license:update-file-header + +### Keep 3rd party licenses file updated + +If you add or remove dependencies then don't forget to run + +mvn license:add-third-party + to update the file: LICENSE.3RD-PARTY \ No newline at end of file diff --git a/LICENSE b/LICENSE index 38b50810..ed9097ea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -The MIT License - -Copyright (c) 2018 SK ID Solutions AS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +The MIT License + +Copyright (c) 2018 SK ID Solutions AS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.3RD-PARTY b/LICENSE.3RD-PARTY index f9a2bee8..4e788d46 100644 --- a/LICENSE.3RD-PARTY +++ b/LICENSE.3RD-PARTY @@ -1,114 +1,114 @@ -List of 112 third-party dependencies (auto-generated on 2025-05-19 with License Maven Plugin): - -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) -* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) -* (Apache License, Version 2.0) jcommander (com.beust:jcommander:1.82 - https://jcommander.org) -* (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) -* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) -* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.17.1 - https://github.com/FasterXML/jackson) -* (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1 - https://github.com/FasterXML/jackson-dataformats-text) -* (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) -* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) -* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) -* (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) -* (BSD 3-clause License w/nuclear disclaimer) Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) -* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) -* (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.3.1 - https://github.com/jknack/handlebars.java/handlebars) -* (The Apache Software License, Version 2.0) Handlebars Helpers (com.github.jknack:handlebars-helpers:4.3.1 - https://github.com/jknack/handlebars.java/handlebars-helpers) -* (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.26.1 - https://errorprone.info/error_prone_annotations) -* (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess) -* (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) -* (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) -* (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) -* (The Apache Software License, Version 2.0) ZXing Core (com.google.zxing:core:3.5.3 - https://github.com/zxing/zxing/core) -* (The Apache Software License, Version 2.0) ZXing Java SE extensions (com.google.zxing:javase:3.5.3 - https://github.com/zxing/zxing/javase) -* (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) -* (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) -* (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) -* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) -* (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.16.1 - https://commons.apache.org/proper/commons-codec/) -* (Apache-2.0) Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) -* (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.11.0 - https://commons.apache.org/proper/commons-io/) -* (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.1 - https://commons.apache.org/proper/commons-logging/) -* (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) -* (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) -* (The Apache Software License, Version 2.0) Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) -* (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) -* (EPL-2.0) (GPL-2.0-with-classpath-exception) Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:4.0.0 - https://github.com/jakartaee/rest/jakarta.ws.rs-api) -* (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) -* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.15.0 - https://bytebuddy.net/byte-buddy) -* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.15.0 - https://bytebuddy.net/byte-buddy-agent) -* (The Apache Software License, Version 2.0) json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.40.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) -* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) -* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) -* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) -* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) -* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) -* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) -* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) -* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) -* (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) -* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) -* (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/) -* (EDL 1.0) Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:11.0.20 - https://eclipse.dev/jetty/jetty-client) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:11.0.20 - https://eclipse.dev/jetty/jetty-http) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:11.0.20 - https://eclipse.dev/jetty/jetty-io) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:11.0.20 - https://eclipse.dev/jetty/jetty-proxy) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Security (org.eclipse.jetty:jetty-security:11.0.20 - https://eclipse.dev/jetty/jetty-security) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:11.0.20 - https://eclipse.dev/jetty/jetty-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:11.0.20 - https://eclipse.dev/jetty/jetty-servlet) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:11.0.20 - https://eclipse.dev/jetty/jetty-servlets) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:11.0.20 - https://eclipse.dev/jetty/jetty-util) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:11.0.20 - https://eclipse.dev/jetty/jetty-webapp) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:11.0.20 - https://eclipse.dev/jetty/jetty-xml) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-common) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-hpack) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-server) -* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api) -* (EPL 2.0) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) -* (EPL 2.0) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) -* (EPL 2.0) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) -* (EPL 2.0) (GPL2 w/ CPE) OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) -* (EPL 2.0) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) -* (Eclipse Distribution License - v 1.0) JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-apache-connector) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) -* (Apache License, 2.0) (EPL 2.0) (Public Domain) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) -* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) -* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) -* (BSD-3-Clause) Hamcrest (org.hamcrest:hamcrest:3.0 - http://hamcrest.org/JavaHamcrest/) -* (BSD-3-Clause) Hamcrest Core (org.hamcrest:hamcrest-core:3.0 - http://hamcrest.org/JavaHamcrest/) -* (BSD-3-Clause) Hamcrest Library (org.hamcrest:hamcrest-library:3.0 - http://hamcrest.org/JavaHamcrest/) -* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) -* (Apache License, Version 2.0) Java Annotation Indexer (org.jboss:jandex:2.4.5.Final - http://www.jboss.org/jandex) -* (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.3.Final - http://www.jboss.org) -* (Apache License 2.0) RESTEasy Client (org.jboss.resteasy:resteasy-client:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Client API (org.jboss.resteasy:resteasy-client-api:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Core (org.jboss.resteasy:resteasy-core:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Core SPI (org.jboss.resteasy:resteasy-core-spi:6.2.10.Final - https://resteasy.dev) -* (Apache License 2.0) RESTEasy Jackson 2 Provider (org.jboss.resteasy:resteasy-jackson2-provider:6.2.10.Final - https://resteasy.dev) -* (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.11.0 - https://junit.org/junit5/) -* (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.11.0 - https://junit.org/junit5/) -* (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.0 - https://junit.org/junit5/) -* (MIT) mockito-core (org.mockito:mockito-core:5.13.0 - https://github.com/mockito/mockito) -* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis) -* (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) -* (MIT-0) reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) -* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:2.0.16 - http://www.slf4j.org) -* (The Apache Software License, Version 2.0) WireMock (org.wiremock:wiremock:3.9.1 - http://wiremock.org) -* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) -* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.10.0 - https://www.xmlunit.org/) -* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.10.0 - https://www.xmlunit.org/xmlunit-placeholders/) -* (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) +List of 112 third-party dependencies (auto-generated on 2025-05-19 with License Maven Plugin): + +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.8 - http://logback.qos.ch/logback-classic) +* (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.8 - http://logback.qos.ch/logback-core) +* (Apache License, Version 2.0) jcommander (com.beust:jcommander:1.82 - https://jcommander.org) +* (Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.2 - https://github.com/ethlo/itu) +* (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.17.2 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.17.2 - https://github.com/FasterXML/jackson-core) +* (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.17.1 - https://github.com/FasterXML/jackson) +* (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1 - https://github.com/FasterXML/jackson-dataformats-text) +* (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) +* (The Apache Software License, Version 2.0) Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.15.4 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) +* (The Apache Software License, Version 2.0) Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.17.1 - https://github.com/FasterXML/jackson-modules-base) +* (BSD 3-clause License w/nuclear disclaimer) Java Advanced Imaging Image I/O Tools API core (standalone) (com.github.jai-imageio:jai-imageio-core:1.4.0 - https://github.com/jai-imageio/jai-imageio-core) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) json-patch (com.github.java-json-tools:json-patch:1.13 - https://github.com/java-json-tools/json-patch) +* (Apache Software License, version 2.0) (Lesser General Public License, version 3 or greater) msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) +* (The Apache Software License, Version 2.0) Handlebars (com.github.jknack:handlebars:4.3.1 - https://github.com/jknack/handlebars.java/handlebars) +* (The Apache Software License, Version 2.0) Handlebars Helpers (com.github.jknack:handlebars-helpers:4.3.1 - https://github.com/jknack/handlebars.java/handlebars-helpers) +* (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.26.1 - https://errorprone.info/error_prone_annotations) +* (The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess) +* (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.2.1-jre - https://github.com/google/guava) +* (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) +* (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/) +* (The Apache Software License, Version 2.0) ZXing Core (com.google.zxing:core:3.5.3 - https://github.com/zxing/zxing/core) +* (The Apache Software License, Version 2.0) ZXing Java SE extensions (com.google.zxing:javase:3.5.3 - https://github.com/zxing/zxing/javase) +* (The Apache Software License, Version 2.0) asyncutil (com.ibm.async:asyncutil:0.1.0 - http://github.com/ibm/java-async-util) +* (The Apache Software License, Version 2.0) json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) +* (Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.0 - https://github.com/networknt/json-schema-validator) +* (Eclipse Distribution License - v 1.0) istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) +* (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.16.1 - https://commons.apache.org/proper/commons-codec/) +* (Apache-2.0) Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) +* (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.11.0 - https://commons.apache.org/proper/commons-io/) +* (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.1 - https://commons.apache.org/proper/commons-logging/) +* (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) +* (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) +* (The Apache Software License, Version 2.0) Jakarta Dependency Injection (jakarta.inject:jakarta.inject-api:2.0.1 - https://github.com/eclipse-ee4j/injection-api) +* (Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) +* (EPL-2.0) (GPL-2.0-with-classpath-exception) Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:4.0.0 - https://github.com/jakartaee/rest/jakarta.ws.rs-api) +* (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) +* (Apache License, Version 2.0) Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.15.0 - https://bytebuddy.net/byte-buddy) +* (Apache License, Version 2.0) Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.15.0 - https://bytebuddy.net/byte-buddy-agent) +* (The Apache Software License, Version 2.0) json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.40.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) +* (The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) +* (The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) +* (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) +* (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) +* (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.3.1 - https://hc.apache.org/httpcomponents-client-5.0.x/5.3.1/httpclient5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5/) +* (Apache License, Version 2.0) Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.2.4 - https://hc.apache.org/httpcomponents-core-5.2.x/5.2.4/httpcore5-h2/) +* (The Apache License, Version 2.0) org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) +* (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.78.1 - https://www.bouncycastle.org/java.html) +* (The MIT License) Checker Qual (org.checkerframework:checker-qual:3.42.0 - https://checkerframework.org/) +* (EDL 1.0) Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-java-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:11.0.20 - https://eclipse.dev/jetty/jetty-alpn-parent/jetty-alpn-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:11.0.20 - https://eclipse.dev/jetty/jetty-client) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Http Utility (org.eclipse.jetty:jetty-http:11.0.20 - https://eclipse.dev/jetty/jetty-http) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: IO Utility (org.eclipse.jetty:jetty-io:11.0.20 - https://eclipse.dev/jetty/jetty-io) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Proxy (org.eclipse.jetty:jetty-proxy:11.0.20 - https://eclipse.dev/jetty/jetty-proxy) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Security (org.eclipse.jetty:jetty-security:11.0.20 - https://eclipse.dev/jetty/jetty-security) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Server Core (org.eclipse.jetty:jetty-server:11.0.20 - https://eclipse.dev/jetty/jetty-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:11.0.20 - https://eclipse.dev/jetty/jetty-servlet) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:11.0.20 - https://eclipse.dev/jetty/jetty-servlets) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Utilities (org.eclipse.jetty:jetty-util:11.0.20 - https://eclipse.dev/jetty/jetty-util) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:11.0.20 - https://eclipse.dev/jetty/jetty-webapp) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:11.0.20 - https://eclipse.dev/jetty/jetty-xml) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-common) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-hpack) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 2.0) Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:11.0.20 - https://eclipse.dev/jetty/http2-parent/http2-server) +* (Apache Software License - Version 2.0) (Eclipse Public License - Version 1.0) Jetty :: Jakarta Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2 - https://eclipse.org/jetty/jetty-jakarta-servlet-api) +* (EPL 2.0) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) +* (EPL 2.0) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) +* (EPL 2.0) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) +* (EPL 2.0) (GPL2 w/ CPE) OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) +* (EPL 2.0) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) +* (Eclipse Distribution License - v 1.0) JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Eclipse Distribution License - v 1.0) TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-connectors-apache (org.glassfish.jersey.connectors:jersey-apache-connector:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-apache-connector) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) +* (Apache License, 2.0) (EPL 2.0) (Public Domain) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-ext-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-entity-filtering) +* (Apache License, 2.0) (BSD 2-Clause) (EDL 1.0) (EPL 2.0) (GPL2 w/ CPE) (MIT license) (Modified BSD) (Public Domain) (W3C license) (jQuery license) jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) +* (Apache License, 2.0) (EPL 2.0) (The GNU General Public License (GPL), Version 2, With Classpath Exception) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:3.1.8 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-json-jackson) +* (BSD-3-Clause) Hamcrest (org.hamcrest:hamcrest:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Core (org.hamcrest:hamcrest-core:3.0 - http://hamcrest.org/JavaHamcrest/) +* (BSD-3-Clause) Hamcrest Library (org.hamcrest:hamcrest-library:3.0 - http://hamcrest.org/JavaHamcrest/) +* (Apache License 2.0) (LGPL 2.1) (MPL 1.1) Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) +* (Apache License, Version 2.0) Java Annotation Indexer (org.jboss:jandex:2.4.5.Final - http://www.jboss.org/jandex) +* (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.3.Final - http://www.jboss.org) +* (Apache License 2.0) RESTEasy Client (org.jboss.resteasy:resteasy-client:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Client API (org.jboss.resteasy:resteasy-client-api:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core (org.jboss.resteasy:resteasy-core:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Core SPI (org.jboss.resteasy:resteasy-core-spi:6.2.10.Final - https://resteasy.dev) +* (Apache License 2.0) RESTEasy Jackson 2 Provider (org.jboss.resteasy:resteasy-jackson2-provider:6.2.10.Final - https://resteasy.dev) +* (Eclipse Public License v2.0) JUnit Jupiter API (org.junit.jupiter:junit-jupiter-api:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Jupiter Params (org.junit.jupiter:junit-jupiter-params:5.11.0 - https://junit.org/junit5/) +* (Eclipse Public License v2.0) JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.0 - https://junit.org/junit5/) +* (MIT) mockito-core (org.mockito:mockito-core:5.13.0 - https://github.com/mockito/mockito) +* (Apache License, Version 2.0) Objenesis (org.objenesis:objenesis:3.3 - http://objenesis.org/objenesis) +* (The Apache License, Version 2.0) org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) +* (MIT-0) reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) +* (MIT License) SLF4J API Module (org.slf4j:slf4j-api:2.0.16 - http://www.slf4j.org) +* (The Apache Software License, Version 2.0) WireMock (org.wiremock:wiremock:3.9.1 - http://wiremock.org) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) +* (The BSD 3-Clause License) org.xmlunit:xmlunit-legacy (org.xmlunit:xmlunit-legacy:2.10.0 - https://www.xmlunit.org/) +* (The Apache Software License, Version 2.0) org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.10.0 - https://www.xmlunit.org/xmlunit-placeholders/) +* (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.2 - https://bitbucket.org/snakeyaml/snakeyaml) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d8e4d9f8..67bf42f2 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,75 +1,75 @@ -# Intro - -Library v3.1 supports only Smart-ID v3 API. -All the previous v2 related code has been removed and all the code necessary for Smart-ID API v3 is under package smartid. -Some classes could also be used in v3 and for those classes the package did not change. - -# Migrating from Smart-ID v2 to Smart-ID v3 API - -## Migrating authentication - -Smart-ID v3 authentication offers new methods to construct builders `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create authentication session request builders. -It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. - -### Overview of V2 authentication flow - -1. Create authentication hash -2. Generate verification code from authentication hash -3. Verification code can be shown to the user -4. Create builder and set values. -5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. -6. After session status is `COMPLETE` response will be checked in the build method. -7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. - -### Moving to V3 authentication flow - -1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` -2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` -3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. - - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. -5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. -6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. - -## Migrating signing - -Signing migration will be focusing on moving to signature flow when device link authentication has been completed before. - -### Overview of V2 signing flow - -1. Set values for certificate choice builder and call build method. Should return certificate as a response. -2. Use queried certificate to create DataToSign object. Requires DigiDoc4j library. -3. Create SignableData from DataToSign. -4. Create verification code from SignableData -5. Create signature builder and set values. -6. Call build method (`sign()`) to create signing session and to start polling for session status. -7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. - -### Moving to V3 signing flow - with DigiDoc4j library - -DigiDoc4j library does not currently support signing with signature algorithm RSASSA-PSS. Support will be added in the future. -There is a possible workaround to use DigiDoc4j library and DSS library together to create ASICS container and sign it with Smart-ID v3 API. -Steps below include examples how to set up DataToSign for signing with RSASSA-PSS and how validate returned signature value. - -#### Steps to migrate - -1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). -2. Use `SignableData` to create digested value for signing. Example for setting up DataToSign with DSS: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdDeviceLinkSignatureService.java#L181 -3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -5. Poll for session status until its complete. -6. Validate session response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. -7. Validate signature value. Example for validating signature value: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdSignatureService.java#L65 - -### Moving to V3 signing flow without DigiDoc4j library - -NB! Without DigiDoc4j library integrator has to provide implementation for creating signed container. -Smart-id-java-client only provides means to validate that signature response has required fields and returned signature value is valid. - -1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). -2. Use `SignableData` to create digested value for signing. -3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. -4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. -5. Poll for session status until its complete. -6. Validate session status response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. -7. Validate signature value with `SignatureValueValidator` +# Intro + +Library v3.1 supports only Smart-ID v3 API. +All the previous v2 related code has been removed and all the code necessary for Smart-ID API v3 is under package smartid. +Some classes could also be used in v3 and for those classes the package did not change. + +# Migrating from Smart-ID v2 to Smart-ID v3 API + +## Migrating authentication + +Smart-ID v3 authentication offers new methods to construct builders `createDeviceLinkAuthentication()` and `createNotificationAuthentication()` to create authentication session request builders. +It is recommended to start using device-link authentication flows from Smart-ID API v3 as these are more secure. + +### Overview of V2 authentication flow + +1. Create authentication hash +2. Generate verification code from authentication hash +3. Verification code can be shown to the user +4. Create builder and set values. +5. Call build method (`authenticate()`) to create authentication session and to start polling for session status. +6. After session status is `COMPLETE` response will be checked in the build method. +7. Use `AuthenticationResponseValidator` to validate the certificate and the signature in the response. + +### Moving to V3 authentication flow + +1. Replace generating authentication hash with generating RP challenge using `RpChallengeGenerator.generate()` +2. [Create device-link authentication builder and set values](README.md#examples-of-initiating-a-device-link-authentication-session) and start authentication session by calling build-method `initAuthenticationSession()` +3. Replace showing verification code with showing device link or QR-code. Recommended to use device link for same device and QR-code for cross-device authentication. + - [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +4. Querying session status can be done in parallel while displaying device content. Check out [session status poller](README.md#example-of-using-session-status-poller-to-query-final-sessions-status). `ee.sk.smartid.SmartIdClient` provides method `getSessionsStatusPoller()` to get version specific session status poller. +5. When session status state is `COMPLETE` polling will be stopped and [response should be checked](README.md#example-of-validating-the-authentication-sessions-response) with `AuthenticationResponseValidator`. It will validate required fields, certificate and signature value in sessions status, and it will also handler errors. +6. If everything is ok `AuthenticationIdentity` will be returned. AuthenticationIdentity is same as used for V2. + +## Migrating signing + +Signing migration will be focusing on moving to signature flow when device link authentication has been completed before. + +### Overview of V2 signing flow + +1. Set values for certificate choice builder and call build method. Should return certificate as a response. +2. Use queried certificate to create DataToSign object. Requires DigiDoc4j library. +3. Create SignableData from DataToSign. +4. Create verification code from SignableData +5. Create signature builder and set values. +6. Call build method (`sign()`) to create signing session and to start polling for session status. +7. After session status is `COMPLETE` response will be checked in the build method. And signed document will be returned. + +### Moving to V3 signing flow - with DigiDoc4j library + +DigiDoc4j library does not currently support signing with signature algorithm RSASSA-PSS. Support will be added in the future. +There is a possible workaround to use DigiDoc4j library and DSS library together to create ASICS container and sign it with Smart-ID v3 API. +Steps below include examples how to set up DataToSign for signing with RSASSA-PSS and how validate returned signature value. + +#### Steps to migrate + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. Example for setting up DataToSign with DSS: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdDeviceLinkSignatureService.java#L181 +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value. Example for validating signature value: https://github.com/SK-EID/smart-id-java-demo/blob/81880330822f7d86a9205e597f24bca42c72d87b/src/main/java/ee/sk/siddemo/services/SmartIdSignatureService.java#L65 + +### Moving to V3 signing flow without DigiDoc4j library + +NB! Without DigiDoc4j library integrator has to provide implementation for creating signed container. +Smart-id-java-client only provides means to validate that signature response has required fields and returned signature value is valid. + +1. Replace certificate choice builder with`CertificateByDocumentNumberRequestBuilder`. SmartID client `ee.sk.smartid.SmartIdClient` provides method `createCertificateByDocumentNumber()` for easier access. Call build method `.getCertificateByDocumentNumber()` to get the certificate. Checkout example [here](README.md#example-of-querying-certificate-by-document-number). +2. Use `SignableData` to create digested value for signing. +3. Use `ee.sk.smartid.SmartIdClient` to [create session request builder](README.md#examples-of-initiating-a-device-link-signature-session) `createDeviceLinkSignature()` and call build method `initSignatureSession()` to start the signing session. +4. Replace showing verification code with showing device link or QR-code. [Create device link or QR-code](README.md#generating-qr-code-or-device-link) from values in session response and display it to the user. QR-code should be recreated after every second. +5. Poll for session status until its complete. +6. Validate session status response with `SignatureResponseValidator`. `SignatureSessionResponse` will be returned when everything is ok. +7. Validate signature value with `SignatureValueValidator` diff --git a/README.md b/README.md index d51a2853..80a7df8a 100644 --- a/README.md +++ b/README.md @@ -1,1537 +1,1537 @@ -[![Tests](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml/badge.svg)](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml) -[![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) -[![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) -[![License: MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) - -# Smart-ID Java client - -This library supports Smart-ID API v3.1. - -## Table of contents - -* [Smart-ID Java client](#smart-id-java-client) - * [Introduction](#introduction) - * [Features](#features) - * [Requirements](#requirements) - * [Getting the library](#getting-the-library) - * [Changelog](#changelog) -* [How to use it with RP API v3.1](#how-to-use-api-v31) - * [Test accounts for testing](#test-accounts-for-testing) - * [Logging](#logging) - * [Log request payloads](#log-request-payloads) - * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) - * [Device link flows](#device-link-flows) - * [Device link authentication session](#device-link-authentication-session) - * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) - * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) - * [Initiating a device link-based authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) - * [Initiating a device link-based authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) - * [Device-link signature session](#device-link-signature-session) - * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) - * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) - * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) - * [Examples of allowed device-link interaction](#examples-of-device-link-interactions) - * [Additional request properties](#additional-device-link-session-request-properties) - * [Generating QR-code or device link](#generating-qr-code-or-device-link) - * [Generating device link ](#generating-device-link) - * [Device link parameters](#device-link-parameters) - * [Overriding default values](#overriding-default-values) - * [Generating QR-code](#generating-qr-code) - * [Generate QR-code Data URI](#generate-qr-code-data-uri) - * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) - * [Callback URL validation](#validating-callback-url) - * [Querying sessions status](#session-status-request-handling-for-v31) - * [Sessions status response](#session-status-response) - * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) - * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) - * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) - * [Validating sessions status response](#validating-session-status-response) - * [Setting up CertificateValidator](#set-up-certificatevalidator) - * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) - * [Example of validating device link-based authentication session status](#device-link-based-authentication-session-status-validation) - * [Example of validating notification-based authentication session status](#notification-based-authentication-session-status-validation) - * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) - * [Example of validating the signature](#example-of-validating-the-signature-session-response) - * [Error handling for session status](#error-handling-for-session-status) - * [Certificate by document number](#certificate-by-document-number) - * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) - * [Linked signature session flow](#linked-signature-flow) - * [Device link certificate choice session](#device-link-certificate-choice-session) - * [Examples of initiating a device-link certificate choice session](#example-of-initiating-a-device-link-certificate-choice-session) - * [Linked notification-based signature session](#linked-notification-based-signature-session) - * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) - * [Notification-based flows](#notification-based-flows) - * [Differences between notification-based, device link-based flows and linked flows](#differences-between-notification-based-device-link-based-and-linked-flows) - * [Notification-based authentication session](#notification-based-authentication-session) - * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) - * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) - * [Initiating notification authentication session with semantics identifier](#initiating-a-notification-based-authentication-session-with-semantics-identifier) - * [Notification-based certificate choice session](#notification-based-certificate-choice-session) - * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) - * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) - * [Notification-based signature session](#notification-based-signature-session) - * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) - * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) - * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) - * [Examples of allowed notification-based interactions order](#examples-of-notification-based-interactions-order) - * [Exception handling](#exception-handling) - * [Network connection configuration of the client](#network-connection-configuration-of-the-client) - * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) - -## Introduction - -The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. -This library supports Smart-ID API v3.1. - -## Features - -* user authentication -* obtain user's signing certificate -* creating digital signature - -## Requirements - * Java 17 or 21 - -## Getting the library - -### Maven -You can use the library as a Maven dependency from the [Maven Central](https://search.maven.org/search?q=a:smart-id-java-client). - -```xml - - ee.sk.smartid - smart-id-java-client - INSERT_VERSION_HERE - -``` - -### Gradle - -`implementation 'ee.sk.smartid:smart-id-java-client:INSERT_VERSION_HERE'` - -## Changelog - -Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) - -# How to use API v3.1 - -Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. -This version introduces new device link and notification-based flows for authentication, certificate choice and signing. - -NB! v2 API classes are removed. - -To use the v3.1 API, import the relevant classes from the ee.sk.smartid package. - -```java - -import ee.sk.smartid.SmartIdConnector; -``` - -## Test accounts for testing - -[Test accounts for testing](https://sk-eid.github.io/smart-id-documentation/test_accounts.html) - - -## Logging - -### Log request payloads - -To log requests going to Smart-ID API set ee.sk.smartid.rest.LoggingFilter to log at trace level. -For applications on Spring Boot this can be done by adding following line to application.yml: -``` -logging.level.ee.sk.smartid.rest.LoggingFilter: trace -``` - -## Setting up SmartIdClient for v3.1 - -[Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) -NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO - -### Setting up SSL connection to Smart-ID API - -Live SSL certificates of Smart-ID service provider (SK) can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_rp_api_smart_id_com_certificates -Demo SSL certificates can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_sid_demo_sk_ee_certificates - -Recommended way is to use truststore and provide it to the client. - -```java -// Read truststore containing Smart-ID service provider (SK) SSL certificates -InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); -KeyStore trustStore = KeyStore.getInstance("JKS"); -trustStore.load(is, "changeit".toCharArray()); - -// Initialize SmartIdClient and set connection parameters. -var smartIdClient = new SmartIdClient(); -// set relying party details -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -// set Smart-ID API host URL -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -// set the trust store containing SK SSL certificates -client.setTrustStore(trustStore); -``` - -### Provide SSL certificates to the client - -Also it is possible to add trusted certificates one by one. - -```java -// Initialize SmartIdClient and set connection parameters. -var smartIdClient = new SmartIdClient(); -// set relying party details -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -// set Smart-ID API host URL -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -// add trusted SSL certificates -client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj..."); -``` - -## Device-link flows - -Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. -More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html - -### Device-link authentication session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required base URI used to form device link or QR code. - -#### Examples of initiating a device-link authentication session - -##### Initiating an anonymous authentication session - -Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to authenticate users without knowing their identity. -RP can learn the user's identity only after the user has authenticated themselves. - -```java -// For security reasons a new hash value must be created for each new authentication request -String rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Set up builder -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link authentication session with semantics identifier - -More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "30303039914"); // identifier (according to country and identity type reference) - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link authentication session with document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating OK authentication sessions status response - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )); - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - - -##### Initiating a device-link authentication session with document number for Web2App flow - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a new rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating OK authentication sessions status response - -// Generate callback URL to be used for same device flows(Web2App, App2App) -CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("your-app://callback"); - -DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Set initial callback URL in the session request - -// Initiate authentication session -DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - -// Get authentication session request used for starting the authentication session and use it later to validate sessions status response -AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - -// Use sessionID to start polling for session status -String sessionId = authenticationSessionResponse.sessionID(); - -// Following values are used for generating device link or QR-code -String sessionToken = authenticationSessionResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = authenticationSessionResponse.sessionSecret(); -URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); -// Will be used to calculate elapsed time being used in QR-code -Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - -// Next steps: -// - Generate QR-code or device link to be displayed to the user -// - Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. -Jump to [Validate callback URL](#validating-callback-url) for more info about validating callback URL. - -### Device-link signature session - -#### Request Parameters - -The request parameters for the device-link signature session are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response Parameters - -The response from a successful device-link signature session creation contains the following parameters: - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. - -#### Examples of initiating a device-link signature session - -##### Initiating a device-link signature session with semantics identifier - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Create the Semantics Identifier -var semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// Initiate the device-link signature -DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) - .initSignatureSession(); - -// Process the signature response -String sessionID = signatureResponse.sessionID(); -String sessionToken = signatureResponse.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.sessionSecret(); -Instant receivedAt = signatureResponse.receivedAt(); -String deviceLinkBase = signatureResponse.deviceLinkBase(); - -// Generate QR-code or device link to be displayed to the user -// Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a device-link signature session with document number - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Specify the document number -String documentNumber = "PNOEE-40504040001-MOCK-Q"; - -// Build the device-link signature request -DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withHashAlgorithm(HashAlgorithm.SHA_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// Process the signature response -String sessionID = signatureResponse.sessionID(); -String sessionToken = signatureResponse.sessionToken(); - -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = signatureResponse.sessionSecret(); -Instant receivedAt = signatureResponse.receivedAt(); -String deviceLinkBase = signatureResponse.deviceLinkBase(); - -// Generate QR-code or device link to be displayed to the user -// Start querying sessions status -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Error Handling -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException` and others. - -```java -try { - DeviceLinkSessionResponse response = builder.init*Session(); -} catch (UserAccountNotFoundException e) { - System.out.println("User account not found."); -} catch (RelyingPartyAccountConfigurationException e) { - System.out.println("Relying party account configuration issue."); -} catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interaction is not supported by the user's app."); -} catch (ServerMaintenanceException e) { - System.out.println("Server maintenance in progress, please try again later."); -} catch (SmartIdClientException e) { - System.out.println("An error occurred: " + e.getMessage()); -} -``` - -### Additional device-link session request properties - -#### Using request properties to request the IP address of the user's device - -For the IP to be returned the service provider (SK) must switch on this option. -More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing - -Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. - -```java -DeviceLinkSessionResponse authenticationSessionResponse = client - .createDeviceLinkAuthentication() - .withRpChallenge(rpChallenge) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - // setting property to request the IP-address of the user's device - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); -``` - -### Examples of device link interactions - -An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. -For device link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. -DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons -and a second screen to enter the PIN-code. - -Below are examples of interaction elements specifically for device link flows: - -Example 1: `confirmationMessage` with fallback to `displayTextAndPIN` -Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. -```java -builder.withInteractions(List.of( - DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), - DeviceLinkInteraction.displayTextAndPin("Up to 60 characters of text here..") -)) -``` - -Example 2: `confirmationMessage` Only (No Fallback) -Description: Insist on `confirmationMessage`; -NB! If interactions is not supported the process will fail if fallback is not provided. -```java -builder.withInteractions(List.of( - DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") -)); -``` - -### Generating QR-code or device link - -Documentation to device link and QR-code requirements -https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html - -To use the Smart-ID **demo environment**, you must specify `smart-id-demo` as `schemeName`. -See: https://sk-eid.github.io/smart-id-documentation/environments.html#_demo - -#### Generating device link - -Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. - -##### Device link parameters - -* `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. -* `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. -* `version`: Version of the device link. Only allowed value is `"1.0"`. -* `deviceLinkType`: Type of the device link. Possible values are `QR`, `Web2App`, `App2App`. -* `sessionType`: Type of the sessions the device link is for. Possible values are `auth`, `sign`, `cert`. -* `sessionToken`: Token from the session response. -* `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` -* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a device link -* `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. -* `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. -* `interactions`: Base64-encoded JSON string of an array of interaction objects, used to calculate the authCode. -* `initialCallbackUrl`: Optional. Initial callback URL used for the same device(Web2App or App2App) device link flows. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. - -```java -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; - -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds since session response -long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); -``` - -##### Overriding default values - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withSchemeName("smart-id-demo") // override default scheme name to use demo environment - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .withInitialCallbackUrl("https://your-app/callback") - .buildDeviceLink(sessionResponse.sessionSecret()); -``` - -#### Generating QR-code - -Creating a QR code uses the Zxing library to generate a QR code image with device link as content. -According to link size the QR-code of version 9 (53x53 modules) is used. -For the QR-code to be scannable by most devices the QR code module size should be ~10px. -It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px). -Generated QR code will have error correction level low. - -##### Generate QR-code Data URI - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withElapsedSeconds(elapsedSeconds) - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); - -// Generate QR code image from device link URI -String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); -// Return Data URI to frontend and display the QR-code -``` - -##### Generate QR-code with custom height, width, quiet area and image format - -Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. -QR code version 9 (53x53 modules) is automatically selected by content size - -Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. -The width and height of 1159px produce a QR code with a module size of 19px. - -```java -DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. -DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. -// Calculate elapsed seconds from response received time -long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); -// Build final device link URI with authCode -URI deviceLink = new DeviceLinkBuilder() - .withDeviceLinkBase(sessionResponse.deviceLinkBase()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionResponse.sessionToken()) - .withLang("est") // override language - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withElapsedSeconds(elapsedSeconds) - .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session - .buildDeviceLink(sessionResponse.sessionSecret()); - -// Create QR-code with height and width of 570px and quiet area of 2 modules. -BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); -// Return Data URI to frontend and display the QR-code -String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); -``` -### Validating callback URL - -When using same device flows (Web2App or App2App) the initialCallbackUrl will be used by the Smart-ID app to redirect the user back to the Relying Party application. -Received callback URL will contain additional query parameters that must be validated by the Relying Party. - -Example of received callback URL for authentication: -`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc&userChallengeVerifier=XtPfaGa8JnGtYrJjboooUf0KfY9sMEHrWFpSQrsUv9c` - -Example of received callback URL for signature or certificate choice -`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc` - -1. RP must verify that the user sessions has `callbackUrl.urlToken()` with same value as in query parameter `value`. -2. RP must verify that the `sessionSecretDigest` query parameter matches the calculated digest created from session secret received in device link session init response. - For this library provides `CallbackUrlUtil.validateSessionSecretDigest(digestFromCallbackUrl, sessionSecret)` -3. For authentication same device flow RP also must verify the `userChallengeVerifier` query parameter. This can be done when polling the session status has finished and session status response has to be - validated. `deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, brokeredRpName);` - Value to validate `userChallengeVerifier` is in the session status response `signature.userChallenge`. - -## Session status request handling for v3.1 - -The Smart-ID v3.1 API includes new session status request path for retrieving session results. -Session status request is to be used for device link-based and notification-based flows. - -### Session status response - -The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: - -* `state`: RUNNING or COMPLETE -* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) -* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. -* `result.details`: Contains additional info when user refused interaction -* `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) -* `signature`: Contains the following fields based on the signatureProtocol used: - * For `ACSP_V2`: value, serverRandom, userChallenge, flowType, signatureAlgorithm, signatureAlgorithmParameters, - * For `RAW_DIGEST_SIGNATURE`: value, flowType, signatureAlgorithm, signatureAlgorithmParameters -* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). -* `ignoredProperties`: Any unsupported or ignored properties from the request. -* `interactionTypeUsed`: The interaction type used for the session. -* `deviceIpAddress`: IP address of the mobile device, if requested. - -### Examples of querying session status in v3.1 - -#### Example of using session status poller to query final sessions status - -The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. - -```java -*SessionResponse sessionResponse; -// Get the session status poller -SessionsStatusPoller poller = client.getSessionsStatusPoller(); - -// Get sessionID from current session response -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) -if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ - System.out.println("Session completed with result: "+sessionStatus.getResult().getEndResult()); -} -``` - -#### Example of querying sessions status only once -The following example shows how to use the SessionStatusPoller to only query the sessions status single time. -NB! If using this method for device link-based flows. Make sure the pollingSleepTimeout is not set or does not impact generating the QR-code for every second. - -```java -*SessionResponse sessionResponse; -// Get the session status poller -SessionStatusPoller poller = client.getSessionStatusPoller(); - -// Querying the sessions status -SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.sessionID()); -// Checking sessions state -if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - // Session is still running and querying can be continued - // Dynamic content can be generated and displayed to the user -} else if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ - // continue to validate the sessions status -} else { - throw UnprocessableSmartIdResponseException("Invalid session state was returned"); -} -``` - -### Validating session status response - -It's important to validate the session status response to ensure that the returned signature or authentication result is valid. -For validating authentication session status response, use the `AuthenticationResponseValidator`. -For validating signature session status response, use the `SignatureResponseValidator`. -NB! Integrators must validate signature value against expected signature value. - -#### Set up CertificateValidator - -CertificateValidator will check if the certificate is not expired and is trusted -by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore. -Will be used by AuthenticationResponseValidator and SignatureResponseValidator. - -```java -// Set up TrustedCACertStore -// Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - -// Option 2 - initialize certificate store with custom locations for trust anchor truststore and for intermediate CA certificates -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() - .withTrustAnchorTruststorePath("path/to/trustAnchorTruststore.jks") - .withTrustAnchorTruststorePassword("password") - .withIntermediateCAStorePath("path/to/intermediateCAStore.jks") - .withIntermediateCAStorePassword("password") - .build(); - - -// Option 3 - Provide trust anchors and intermediate CA certificates directly -Set trustAnchors; -List intermediateCACertificates; -TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() - .withTrustAnchors(trustAnchors) - .withIntermediateCACertificates(intermediateCACertificates) - .build(); - -// Set up CertificateValidator with the trusted CA store -CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); -``` - -#### Example of validating the authentication sessions response: - -##### Device link-based authentication session status validation - -DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -// Set up AuthenticationResponseValidator with the CertificateValidator -DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); - -// Create authentication request builder -DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; -// Initialize session -DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); -// Get request used for starting the authentication session and use it later to validate sessions status response -DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); - -// get sessions result -SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// validate sessions state is completed -if("COMPLETE".equals(sessionStatus.getState())){ - // validate the session status response with authentication session request and return authentication identity - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); -} -``` - -##### Notification-based authentication session status validation - -NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -// Set up AuthenticationResponseValidator with the CertificateValidator -NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); - -// Create authentication request builder -NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; -// Initialize session -NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); -// Get request used for starting the authentication session and use it later to validate sessions status response -NotificationAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); - -// get sessions result -SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); -SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); - -// validate sessions state is completed -if("COMPLETE".equals(sessionStatus.getState())){ - // validate the session status response with authentication session request and return authentication identity - AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); -} -``` - -#### Example of validating the certificate choice session response: - -CertificateChoiceResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -try { - // Set up CertificateChoiceResponseValidator with the CertificateValidator - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(sessionStatus); - -} catch (UserRefusedException e) { - System.out.println("User refused the session."); -} catch (SessionTimeoutException e) { - System.out.println("Session timed out."); -} catch (DocumentUnusableException e) { - System.out.println("Document is unusable for the session."); -} catch (SmartIdClientException e) { - System.out.println("An unexpected error occurred: " + e.getMessage()); -} -``` - -#### Example of validating the signature session response: - -SignatureResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) - -```java -try { - // Objects needed for validation - CertificateResponse certResponse; // queried by document number or use CertificateChoiceResponse - SignableData signableData; // data that was sent for signing - // Initialize the signature response validator with CertificateValidator - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); - // Validate signature value. This step can be skipped if other means of validating the signature value can be used. - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), - signableData.calculateHash(), - certResponse.certificate(), - signatureResponse.getRsaSsaPssParameters()); - - // Process the response (e.g., save to database or pass to another system) - handleSignatureResponse(signatureResponse); - -} catch (UserRefusedException e) { - System.out.println("User refused the session."); -} catch (SessionTimeoutException e) { - System.out.println("Session timed out."); -} catch (DocumentUnusableException e) { - System.out.println("Document is unusable for the session."); -} catch (SmartIdClientException e) { - System.out.println("An unexpected error occurred: " + e.getMessage()); -} -``` - -### Error handling for session status - -The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: - -* `OK`: Session completed successfully. -* `USER_REFUSED`: User refused the session. -* `TIMEOUT`: User did not respond in time. -* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. -* `WRONG_VC`: User selected the wrong verification code. -* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. -* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. -* `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction - was canceled. - * `displayTextAndPIN` - User pressed Cancel on PIN screen during displayTextAndPIN flow. - * `confirmationMessage` - User cancelled on confirmationMessage screen. - * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. -* `PROTOCOL_FAILURE`: An error occurred in the signing protocol. -* `EXPECTED_LINKED_SESSION`: RP has configured signature session that should follow device-link certificate choice session incorrectly and the process - cannot be completed. -* `SERVER_ERROR` - Technical error occurred at the server side and the process was terminated. - -## Certificate by document number - -In API v3.1 new endpoint was introduced to simplify querying certificate for signing. -RP can directly query the user's signing certificate by document number — no session flow or user interaction required. -Can be used for device link and notification-based signature flows. -Only requirement is that the device link authentication is successfully completed before to get the document number. - -### Request Parameters -The request parameters for the certificate by document number request are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are `ADVANCED`, `QUALIFIED` or `QSCD`. Defaults to `QUALIFIED`. - -### Response Parameters -* `state`: Required. Indicates result. Possible values: - * `OK`: Certificate found and returned. - * `DOCUMENT_UNUSABLE`: user's Smart-ID account is not usable for signing -* `cert`: Required. Object containing the signing certificate. - * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) - * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` - -### Example of querying certificate by document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// Build the certificate by document number request and query the certificate -CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - -// Set up the certificate validator -TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); -CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - -// Validate the certificate -certificateValidator.validateCertificate(certResponse.certificate()); -``` -Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). - -## Linked signature flow - -In API v3.1 a new flow was introduced to link signature session to a previously completed certificate choice session. -The flow starts off with device link certificate choice session and must be followed by a linked notification-based signature session. - -### Device link certificate choice session - -Anonymous device link certificate choice session can be initiated without knowing the user's document number. When the session is completed successfully, -the Smart-ID API will stay waiting for the RP to start the [linked notification-based signature session](#linked-notification-based-signature-session). - -#### Request Parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotent behaviour. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. -* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. -* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). -* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. -* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. - -#### Example of initiating a device-link certificate choice session - -```java -DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) - .initiateCertificateChoice(); - -String sessionId = certificateChoice.sessionID(); -// SessionID is used to query sessions status later - -String sessionToken = certificateChoice.sessionToken(); -// Store sessionSecret only on backend side. Do not expose it to the client side. -String sessionSecret = certificateChoice.sessionSecret(); -String deviceLinkBase = certificateChoice.deviceLinkBase(); -Instant responseReceivedAt = certificateChoice.receivedAt(); -``` -Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Linked notification-based signature session - -Second part of the linked signature flow. Will be used to start the signature session after the device link certificate choice session is completed successfully. - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `linkedSessionID`: Required. Session ID of the previously completed certificate choice session. -* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. -* `requestProperties`: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response parameters - -* `sessionID`: Required. String that can be used to request the signature session status result. - -#### Example of initiating a linked notification-based signature session - -```java -// Prerequisite: device link certificate choice has been completed successfully. -DeviceLinkSessionResponse certificateChoiceSessionResponse; -CertificateChoiceResponse certificateChoiceResponse; - -// Start the linked notification signature session using the sessionID from the certificate choice session -LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) - .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) - .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// SessionID is used to query sessions status later -String sessionId = signatureSessionResponse.sessionID(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -## Notification-based flows - -### Differences between notification-based, device link-based and linked flows - -* `Notification-Based flow` - * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. - * Known users or devices: - * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using device link-based flows. - * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. -* `Device Link flow` - * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. - * Anonymous authentication: the user's details are not required beforehand. RP validates the user after the Smart-ID authentication is completed. - * Real-time updates: QR-code needs to be refreshed every second to ensure validity. -* `Linked flow` - * Combination of anonymous certificate choice and notification-based signing: Starts with a device link-based certificate choice session followed by a notification-based signing session. - * QR-code or device link will be used only for the certificate choice part of the flow. - * Supports only device link-based interactions in the signature part of the flow. - -### Notification-based authentication session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. -* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. - * `rpChallenge`: Required. Random value with size in range of 32-64 bytes. Must be Base64 encoded. - * `signatureAlgorithm`: Required. Signature algorithm name. Supported values is 'rsassa-pss' - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. An array of interaction objects defining the interactions in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. -* `vcType`: Required. Type of verification code to be used. Currently, the only allowed value is `numeric4`. - -#### Response parameters -* `sessionID`: Required. String used to request the operation result. - -#### Examples of initiating a notification-based authentication session - -##### Initiating a notification-based authentication session with document number - -```java -String documentNumber = "PNOLT-40504040001-MOCK-Q"; - -// For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Generate verification code and display it to the user for confirmation -String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - .initAuthenticationSession(); - -// SessionID is used to query sessions status later -String sessionId = authenticationSessionResponse.sessionID(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a notification-based authentication session with semantics identifier - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// For security reasons a rpChallenge must be created for each new authentication request -RpChallenge rpChallenge = RpChallengeGenerator.generate(); -// Store generated rpChallenge only on backend side. Do not expose it to the client side. -// Used for validating authentication sessions status OK response - -// Generate verification code and display it to the user for confirmation -String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into "))) // Display text should be concise and specific. - .initAuthenticationSession(); - -// SessionID can be used to query sessions status later -String sessionId = authenticationSessionResponse.sessionID(); - -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Notification-based certificate choice session - -#### Request parameters - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. -* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. -* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. -* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. - -#### Response parameters - -* `sessionID`: A string that can be used to request the session status result. - -#### Examples of initiating a notification-based certificate choice session - -##### Initiating a notification-based certificate choice session using semantics identifier - -```java -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - -// Use requested certificate level to validate certificate choice session status OK response. -CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" -NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(requestedCertificateLevel) - .initCertificateChoice(); - -String sessionId = certificateChoiceSessionResponse.sessionID(); -// SessionID is used to query sessions status later -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Notification-based signature session - -#### Request Parameters -The request parameters for the notification-based signature session are as follows: - -* `relyingPartyUUID`: Required. UUID of the Relying Party. -* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. -* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. -* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. -* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. - * `digest`: Required. Base64 encoded digest to be signed. - * `signatureAlgorithm`: Required. Signature algorithm name. Only `rsassa-pss` is currently supported. - * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. - * `hashAlgorithm`: Required. Hash algorithm used for digest. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. -* `interactions`: Required. Base64-encoded string of interactions to be used for a session. The interactions are defined in order of preference. - * Each interaction object includes: - * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. - * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. -* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. To be used for overriding idempotency. -* `requestProperties`: requestProperties: - * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. -* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. - -#### Response Parameters -* `sessionID`: Required. String used to request the operation result. -* `vc`: Required. Object describing the verification code details. - * `type`: Required. Type of the verification code. Currently, the only allowed type is `numeric4`. - * `value`: Required. Value of the verification code to be displayed to the user. - -#### Examples of initiating a notification-based signature session - -##### Initiating a notification-based signature session with semantics identifier - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Create the Semantics Identifier -SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" -); - -// Build the notification signature request -NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the ")) // Display text should be concise and specific. - ) - .initSignatureSession(); - -// Get the session ID and continue to querying session status -String sessionID = signatureSessionResponse.sessionID(); - -// Display verification code to the user -String verificationCode = signatureSessionResponse.vc().getValue(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -##### Initiating a notification-based signature session with document number - -```java -// Create the signable data -var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - -// Specify the document number -String documentNumber = "PNOEE-40504040001-MOCK-Q"; - -// Initiate the session -NotificationSignatureSessionResponse signatureResponse = client.createNotificationSignature() - .withRelyingPartyUUID(client.getRelyingPartyUUID()) - .withRelyingPartyName(client.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withAllowedInteractionsOrder(List.of( - NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. - .initSignatureSession(); - -// Get the session ID and continue to querying session status -String sessionID = signatureResponse.sessionID(); - -// Display verification code to the user -String verificationCode = signatureResponse.vc().getValue(); -``` -Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. - -### Error Handling - -Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: -* `UserAccountNotFoundException` -* `RelyingPartyAccountConfigurationException` -* `SessionNotFoundException` -* `RequiredInteractionNotSupportedByAppException` -* `ServerMaintenanceException` -* `SmartIdClientException` - -#### Example of Error Handling -```java -try { - NotificationSignatureSessionResponse response = builder.initSignatureSession(); -} catch (UserAccountNotFoundException e) { - System.out.println("User account not found."); -} catch (RelyingPartyAccountConfigurationException e) { - System.out.println("Relying party account configuration issue."); -} catch (RequiredInteractionNotSupportedByAppException e) { - System.out.println("The required interaction is not supported by the user's app."); -} catch (ServerMaintenanceException e) { - System.out.println("Server maintenance in progress, please try again later."); -} catch (SmartIdClientException e) { - System.out.println("An error occurred: " + e.getMessage()); -} -``` - -### Additional notification-based session request parameters - -#### Using nonce to override idempotent behaviour - -Idempotent behaviour means that if the session request with same values is made multiple times within a 15-second window, -the same response with identical values will be returned. If there is a need to override this behaviour, a nonce can be used. -Nonce value must be a random string with a minimum length of 1 and a maximum length of 30 characters. - -Notification-based signature request is used as an example. Nonce can also be used with other signing session request -(device-link signature and certificate choice; notification-based certificate choice) by using method `withNonce("randomValue")`. - -```java -NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) - .withRelyingPartyName(smartIdClient.getRelyingPartyName()) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(Collections.singletonList( - NotificationInteraction.confirmationMessage("Please sign the ") // Display text should be concise and specific. - )) - // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned - // set nonce to override idempotent behaviour - .withNonce("randomValue") - .initSignatureSession(); -``` - -#### Using request properties to request the IP address of the user's device - -For the IP to be returned the service provider (SK) must switch on this option. -More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing - -Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. - -```java -NotificationAuthenticationSessionResponse authenticationSessionResponse = client - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. - )) - // setting property to request the IP-address of the user's device - .withShareMdClientIpAddress(true) - .initAuthenticationSession(); -``` - -### Examples of notification-based interactions order - -An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. -Different interactions can support different amounts of data to display information to the user. - -Below are examples of `interactions`. - -Example 1: `confirmationMessageAndVerificationCodeChoice` with fallback to `confirmationMessage` and with fallback to `displayTextAndPIN` -Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; The second choice is `confirmationMessage`; The third choice is `displayTextAndPIN`. -```java -builder.withInteractions(List.of( - NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), - NotificationInteraction.confirmationMessage("Up to 200 characters of text here..."), - NotificationInteraction.displayTextAndPin("Up to 60 characters of text here...") -)); -``` - -Example 2: `confirmationMessageAndVerificationCodeChoice` only -Description: Use `confirmationMessageAndVerificationCodeChoice` interaction exclusively. -NB! Process will fail when interaction is not supported and there is no fallback -```java -builder.withInteractions(List.of( - NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") -)); -``` - -## Exception Handling -The Smart-ID Java client library provides specific exceptions for different error scenarios. Handle exceptions appropriately to provide a good user experience. - -Exception Categories -* Permanent Exceptions - These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input - * `SmartIdClientException` Thrown for general client-side errors, such as: - * Missing or invalid configuration (e.g., `trustSslContext` not set). - * `SmartIdRequestSetupException` Thrown when the request field validations fails, such as: - * Missing required fields (e.g., `relyingPartyUUID`, `relyingPartyName`, `signatureProtocol`). - * Invalid values for fields (e.g. `interactionType` containing duplicate types). -* Unprocessable Response Exceptions - These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. - * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. - * Missing required fields (e.g., `state`, `endResult`, `signatureAlgorithm`). - * Incorrectly encoded Base64 strings in signature or certificate. - * Unexpected or unsupported `signatureProtocol`. -* User Action Exceptions - These exceptions cover scenarios where user actions or inactions lead to session termination or errors. - * `UserRefusedException` Base exception for user refusal scenarios. - * `SessionTimeoutException`: User did not respond within the allowed timeframe. - * `UserSelectedWrongVerificationCodeException` Thrown when the user selects an incorrect verification code during the process. -* User Account Exceptions - These exceptions handle issues related to the user's Smart-ID account or session requirements. - * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. - * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. - * `UserAccountUnusableException` Thrown when the user's Smart-ID account is not currently usable for the requested operation. -* Validation and Parsing Exceptions - These exceptions arise during validation or parsing operations within the library. - * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. - * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. -* Server side exceptions - * `ProtocolFailureException` Thrown when the Smart-ID API received invalid data such (f.e wrong data in generate device link) - * `ExpectedLinkedSessionException` Thrown when the Relying Party did not configure linked signature session to follow anonymous device-link certificate choice session. - * `SmartIdServerException` Thrown when the Smart-ID terminates the process due to a server-side error. - -## Network connection configuration of the client - -Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: - -- Initiation request -- Session status request - -Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. - -```java -SmartIdClient client = new SmartIdClient(); -// ... -// sets the timeout for each session status poll -client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); -// sets the pause between each session status poll -client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); -``` - -As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. - -Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig(); -clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); -clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); - -client.setNetworkConnectionConfig(clientConfig); -``` -And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: - -```java -SmartIdClient client = new SmartIdClient(); -// ... -ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); -RequestConfig reqConfig = RequestConfig.custom() - .setConnectTimeout(5000) - .setSocketTimeout(30000) - .setConnectionRequestTimeout(5000) - .build(); -clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); - -client.setNetworkConnectionConfig(clientConfig); -``` - -Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. - -### Example of creating a client with configured ssl context on JBoss using JAXWS RS - - -```java -ResteasyClient resteasyClient = new ResteasyClientBuilder() - .sslContext(SmartIdClient.createSslContext(Arrays.asList( - "pem cert 1", "pem cert 2"))) - .build(); - -SmartIdClient client = new SmartIdClient(); -client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); -client.setRelyingPartyName("DEMO"); -client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); -client.setConfiguredClient(resteasyClient); +[![Tests](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml/badge.svg)](https://github.com/SK-EID/smart-id-java-client/actions/workflows/tests.yaml) +[![Dependencies](https://img.shields.io/librariesio/release/maven/ee.sk.smartid:smart-id-java-client)](https://libraries.io/maven/ee.sk.smartid:smart-id-java-client) +[![Coverage Status](https://img.shields.io/codecov/c/github/SK-EID/smart-id-java-client.svg)](https://codecov.io/github/SK-EID/smart-id-java-client/) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ee.sk.smartid/smart-id-java-client) +[![License: MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) + +# Smart-ID Java client + +This library supports Smart-ID API v3.1. + +## Table of contents + +* [Smart-ID Java client](#smart-id-java-client) + * [Introduction](#introduction) + * [Features](#features) + * [Requirements](#requirements) + * [Getting the library](#getting-the-library) + * [Changelog](#changelog) +* [How to use it with RP API v3.1](#how-to-use-api-v31) + * [Test accounts for testing](#test-accounts-for-testing) + * [Logging](#logging) + * [Log request payloads](#log-request-payloads) + * [Setting up SmartIdClient for v3.1](#setting-up-smartidclient-for-v31) + * [Device link flows](#device-link-flows) + * [Device link authentication session](#device-link-authentication-session) + * [Examples of authentication session](#examples-of-initiating-a-device-link-authentication-session) + * [Initiating an anonymous authentication session](#initiating-an-anonymous-authentication-session) + * [Initiating a device link-based authentication session with semantics identifier](#initiating-a-device-link-authentication-session-with-semantics-identifier) + * [Initiating a device link-based authentication session with document number](#initiating-a-device-link-authentication-session-with-document-number) + * [Device-link signature session](#device-link-signature-session) + * [Examples of initiating a device-link signature session](#examples-of-initiating-a-device-link-signature-session) + * [Initiating a device-link signature session using semantics identifier](#initiating-a-device-link-signature-session-with-semantics-identifier) + * [Initiating a device-link signature session using document number](#initiating-a-device-link-signature-session-with-document-number) + * [Examples of allowed device-link interaction](#examples-of-device-link-interactions) + * [Additional request properties](#additional-device-link-session-request-properties) + * [Generating QR-code or device link](#generating-qr-code-or-device-link) + * [Generating device link ](#generating-device-link) + * [Device link parameters](#device-link-parameters) + * [Overriding default values](#overriding-default-values) + * [Generating QR-code](#generating-qr-code) + * [Generate QR-code Data URI](#generate-qr-code-data-uri) + * [Generate QR-code with custom height, width, quiet area and image format](#generate-qr-code-with-custom-height-width-quiet-area-and-image-format) + * [Callback URL validation](#validating-callback-url) + * [Querying sessions status](#session-status-request-handling-for-v31) + * [Sessions status response](#session-status-response) + * [Example of querying session status in v3.1](#examples-of-querying-session-status-in-v31) + * [Example of using session status poller to query final sessions status](#example-of-using-session-status-poller-to-query-final-sessions-status) + * [Example of querying sessions status](#example-of-querying-sessions-status-only-once) + * [Validating sessions status response](#validating-session-status-response) + * [Setting up CertificateValidator](#set-up-certificatevalidator) + * [Example of validating authentication session response](#example-of-validating-the-authentication-sessions-response) + * [Example of validating device link-based authentication session status](#device-link-based-authentication-session-status-validation) + * [Example of validating notification-based authentication session status](#notification-based-authentication-session-status-validation) + * [Example of validating certificate session response](#example-of-validating-the-certificate-choice-session-response) + * [Example of validating the signature](#example-of-validating-the-signature-session-response) + * [Error handling for session status](#error-handling-for-session-status) + * [Certificate by document number](#certificate-by-document-number) + * [Example of querying certificate by document number](#example-of-querying-certificate-by-document-number) + * [Linked signature session flow](#linked-signature-flow) + * [Device link certificate choice session](#device-link-certificate-choice-session) + * [Examples of initiating a device-link certificate choice session](#example-of-initiating-a-device-link-certificate-choice-session) + * [Linked notification-based signature session](#linked-notification-based-signature-session) + * [Example of initiating a linked notification-based signature session](#example-of-initiating-a-linked-notification-based-signature-session) + * [Notification-based flows](#notification-based-flows) + * [Differences between notification-based, device link-based flows and linked flows](#differences-between-notification-based-device-link-based-and-linked-flows) + * [Notification-based authentication session](#notification-based-authentication-session) + * [Examples of initiating notification authentication session](#examples-of-initiating-a-notification-based-authentication-session) + * [Initiating notification authentication session with document number](#initiating-a-notification-based-authentication-session-with-document-number) + * [Initiating notification authentication session with semantics identifier](#initiating-a-notification-based-authentication-session-with-semantics-identifier) + * [Notification-based certificate choice session](#notification-based-certificate-choice-session) + * [Examples of initiating notification certificate choice session](#examples-of-initiating-a-notification-based-certificate-choice-session) + * [Initiating notification-based certificate choice with semantics identifier](#initiating-a-notification-based-certificate-choice-session-using-semantics-identifier) + * [Notification-based signature session](#notification-based-signature-session) + * [Examples of initiating notification-based signature session](#examples-of-initiating-a-notification-based-signature-session) + * [Initiating a notification-based signature session with semantics identifier](#initiating-a-notification-based-signature-session-with-semantics-identifier) + * [Initiating a notification-based signature session with document number](#initiating-a-notification-based-signature-session-with-document-number) + * [Examples of allowed notification-based interactions order](#examples-of-notification-based-interactions-order) + * [Exception handling](#exception-handling) + * [Network connection configuration of the client](#network-connection-configuration-of-the-client) + * [Example of creating a client with configured ssl context on JBoss using JAXWS RS](#example-of-creating-a-client-with-configured-ssl-context-on-jboss-using-jaxws-rs) + +## Introduction + +The Smart-ID Java client can be used for easy integration of the [Smart-ID](https://www.smart-id.com) solution to information systems or e-services. +This library supports Smart-ID API v3.1. + +## Features + +* user authentication +* obtain user's signing certificate +* creating digital signature + +## Requirements + * Java 17 or 21 + +## Getting the library + +### Maven +You can use the library as a Maven dependency from the [Maven Central](https://search.maven.org/search?q=a:smart-id-java-client). + +```xml + + ee.sk.smartid + smart-id-java-client + INSERT_VERSION_HERE + +``` + +### Gradle + +`implementation 'ee.sk.smartid:smart-id-java-client:INSERT_VERSION_HERE'` + +## Changelog + +Changes introduced with new library versions are described in [CHANGELOG.md](CHANGELOG.md) + +# How to use API v3.1 + +Support for Smart-ID API v3.1 has been added to the library. The code for v3.1 is located under the ee.sk.smartid package. +This version introduces new device link and notification-based flows for authentication, certificate choice and signing. + +NB! v2 API classes are removed. + +To use the v3.1 API, import the relevant classes from the ee.sk.smartid package. + +```java + +import ee.sk.smartid.SmartIdConnector; +``` + +## Test accounts for testing + +[Test accounts for testing](https://sk-eid.github.io/smart-id-documentation/test_accounts.html) + + +## Logging + +### Log request payloads + +To log requests going to Smart-ID API set ee.sk.smartid.rest.LoggingFilter to log at trace level. +For applications on Spring Boot this can be done by adding following line to application.yml: +``` +logging.level.ee.sk.smartid.rest.LoggingFilter: trace +``` + +## Setting up SmartIdClient for v3.1 + +[Configure to use with Smart-ID Demo environment](https://sk-eid.github.io/smart-id-documentation/environments.html#_demo) +NB! Smart-ID Basic level accounts (certificate level ADVANCED) are not supported for DEMO + +### Setting up SSL connection to Smart-ID API + +Live SSL certificates of Smart-ID service provider (SK) can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_rp_api_smart_id_com_certificates +Demo SSL certificates can be found here: https://sk-eid.github.io/smart-id-documentation/https_pinning.html#_sid_demo_sk_ee_certificates + +Recommended way is to use truststore and provide it to the client. + +```java +// Read truststore containing Smart-ID service provider (SK) SSL certificates +InputStream is = SmartIdClient.class.getResourceAsStream("demo_server_trusted_ssl_certs.jks"); +KeyStore trustStore = KeyStore.getInstance("JKS"); +trustStore.load(is, "changeit".toCharArray()); + +// Initialize SmartIdClient and set connection parameters. +var smartIdClient = new SmartIdClient(); +// set relying party details +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// set the trust store containing SK SSL certificates +client.setTrustStore(trustStore); +``` + +### Provide SSL certificates to the client + +Also it is possible to add trusted certificates one by one. + +```java +// Initialize SmartIdClient and set connection parameters. +var smartIdClient = new SmartIdClient(); +// set relying party details +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +// set Smart-ID API host URL +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +// add trusted SSL certificates +client.setTrustedCertificates("-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIQBH3ZvDVJl5qtCPwQJSruuj..."); +``` + +## Device-link flows + +Device-link flows are more secure way to make sure user that started the authentication or signing is in control of the device or in the proximity of the device. +More info available here https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html + +### Device-link authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED or QUALIFIED. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. + * `rpChallenge`: Required. Base64-encoded value, length between 44 and 88 characters. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported value only `rsassa-pss`. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be set when using same device flows. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required base URI used to form device link or QR code. + +#### Examples of initiating a device-link authentication session + +##### Initiating an anonymous authentication session + +Anonymous authentication is a new feature in Smart-ID API v3.1. It allows to authenticate users without knowing their identity. +RP can learn the user's identity only after the user has authenticated themselves. + +```java +// For security reasons a new hash value must be created for each new authentication request +String rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Set up builder +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link authentication session with semantics identifier + +More info about Semantics Identifier can be found [here](https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.00_30/en_31941201v010100v.pdf) + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "30303039914"); // identifier (according to country and identity type reference) + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link authentication session with document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating OK authentication sessions status response + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )); + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + + +##### Initiating a device-link authentication session with document number for Web2App flow + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a new rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating OK authentication sessions status response + +// Generate callback URL to be used for same device flows(Web2App, App2App) +CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("your-app://callback"); + +DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); // Set initial callback URL in the session request + +// Initiate authentication session +DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + +// Get authentication session request used for starting the authentication session and use it later to validate sessions status response +AuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + +// Use sessionID to start polling for session status +String sessionId = authenticationSessionResponse.sessionID(); + +// Following values are used for generating device link or QR-code +String sessionToken = authenticationSessionResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = authenticationSessionResponse.sessionSecret(); +URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); +// Will be used to calculate elapsed time being used in QR-code +Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + +// Next steps: +// - Generate QR-code or device link to be displayed to the user +// - Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. +Jump to [Validate callback URL](#validating-callback-url) for more info about validating callback URL. + +### Device-link signature session + +#### Request Parameters + +The request parameters for the device-link signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `initialCallbackUrl`: Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains a |, it must be percent-encoded. Should be used for same-device flow. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response Parameters + +The response from a successful device-link signature session creation contains the following parameters: + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect this signature attempt between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. + +#### Examples of initiating a device-link signature session + +##### Initiating a device-link signature session with semantics identifier + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Create the Semantics Identifier +var semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// Initiate the device-link signature +DeviceLinkSessionResponse signatureResponse = client.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); + +// Generate QR-code or device link to be displayed to the user +// Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a device-link signature session with document number + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Specify the document number +String documentNumber = "PNOEE-40504040001-MOCK-Q"; + +// Build the device-link signature request +DeviceLinkSessionResponse signatureResponse = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withHashAlgorithm(HashAlgorithm.SHA_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// Process the signature response +String sessionID = signatureResponse.sessionID(); +String sessionToken = signatureResponse.sessionToken(); + +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = signatureResponse.sessionSecret(); +Instant receivedAt = signatureResponse.receivedAt(); +String deviceLinkBase = signatureResponse.deviceLinkBase(); + +// Generate QR-code or device link to be displayed to the user +// Start querying sessions status +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Error Handling +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as `UserAccountNotFoundException`, `UserRefusedException` and others. + +```java +try { + DeviceLinkSessionResponse response = builder.init*Session(); +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { + System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { + System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { + System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { + System.out.println("An error occurred: " + e.getMessage()); +} +``` + +### Additional device-link session request properties + +#### Using request properties to request the IP address of the user's device + +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing + +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. + +```java +DeviceLinkSessionResponse authenticationSessionResponse = client + .createDeviceLinkAuthentication() + .withRpChallenge(rpChallenge) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) // Certificate level can either be "QUALIFIED" or "ADVANCED" + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); +``` + +### Examples of device link interactions + +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. +For device link flows, the available interaction types are limited to displayTextAndPIN and confirmationMessage. +DisplayTextAndPIN is used for short text with PIN-code input, while confirmationMessage is used for longer text with Confirm and Cancel buttons +and a second screen to enter the PIN-code. + +Below are examples of interaction elements specifically for device link flows: + +Example 1: `confirmationMessage` with fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessage`; if not available, then fall back to `displayTextAndPIN`. +```java +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here.."), + DeviceLinkInteraction.displayTextAndPin("Up to 60 characters of text here..") +)) +``` + +Example 2: `confirmationMessage` Only (No Fallback) +Description: Insist on `confirmationMessage`; +NB! If interactions is not supported the process will fail if fallback is not provided. +```java +builder.withInteractions(List.of( + DeviceLinkInteraction.confirmationMessage("Up to 200 characters of text here..") +)); +``` + +### Generating QR-code or device link + +Documentation to device link and QR-code requirements +https://sk-eid.github.io/smart-id-documentation/rp-api/device_link_flows.html + +To use the Smart-ID **demo environment**, you must specify `smart-id-demo` as `schemeName`. +See: https://sk-eid.github.io/smart-id-documentation/environments.html#_demo + +#### Generating device link + +Device link can be generated for 3 use cases: QR-code, web link to Smart-ID app, app link to Smart-ID app. + +##### Device link parameters + +* `schemeName` : Controls which Smart-ID environment is targeted. Default value is `smart-id`. +* `deviceLinkBase`: Value of `deviceLinkBase` returned in session-init response. +* `version`: Version of the device link. Only allowed value is `"1.0"`. +* `deviceLinkType`: Type of the device link. Possible values are `QR`, `Web2App`, `App2App`. +* `sessionType`: Type of the sessions the device link is for. Possible values are `auth`, `sign`, `cert`. +* `sessionToken`: Token from the session response. +* `elapsedSeconds`: Seconds since the session-init response was received – only for `QR_CODE` +* `lang`: User language. Default value is `eng`. Is used to set language of the fallback page. Fallback page is used for cases when the app is not installed or some other problem occurs with opening a device link +* `digest`: Base64-encoded digest or rpChallenge from session-init. Required for `auth` and `sign` flows. +* `relyingPartyNameBase64`: Base64-encoded relying party name, used for authentication sessions. It is used to calculate the authCode. +* `interactions`: Base64-encoded JSON string of an array of interaction objects, used to calculate the authCode. +* `initialCallbackUrl`: Optional. Initial callback URL used for the same device(Web2App or App2App) device link flows. It must match the regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. + +```java +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; + +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds since session response +long elapsedSeconds = Duration.between(session.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); +``` + +##### Overriding default values + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withSchemeName("smart-id-demo") // override default scheme name to use demo environment + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .withInitialCallbackUrl("https://your-app/callback") + .buildDeviceLink(sessionResponse.sessionSecret()); +``` + +#### Generating QR-code + +Creating a QR code uses the Zxing library to generate a QR code image with device link as content. +According to link size the QR-code of version 9 (53x53 modules) is used. +For the QR-code to be scannable by most devices the QR code module size should be ~10px. +It is achieved by setting the height and width of the QR code to 610px (calculated as (53+2x4)*10px). +Generated QR code will have error correction level low. + +##### Generate QR-code Data URI + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); + +// Generate QR code image from device link URI +String qrCodeDataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); +// Return Data URI to frontend and display the QR-code +``` + +##### Generate QR-code with custom height, width, quiet area and image format + +Notably, the module size in pixels should be more than 5px and less than 20px. The recommended module size is 10px. +QR code version 9 (53x53 modules) is automatically selected by content size + +Other image size in range 366px to 1159px is also possible. Width and height of 366px produce a QR code with a module size of 6px. +The width and height of 1159px produce a QR code with a module size of 19px. + +```java +DeviceLinkSessionResponse sessionResponse; // response from the session initiation query. +DeviceLinkAuthenticationSessionRequest sessionRequest; // request used for starting the authentication or signing session. For example authentication session request is used. +// Calculate elapsed seconds from response received time +long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); +// Build final device link URI with authCode +URI deviceLink = new DeviceLinkBuilder() + .withDeviceLinkBase(sessionResponse.deviceLinkBase()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionResponse.sessionToken()) + .withLang("est") // override language + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withElapsedSeconds(elapsedSeconds) + .withInteractions(sessionRequest.interactions()) // interactions from the authentication or signing session request, should be empty when used with device link certificate choice session + .buildDeviceLink(sessionResponse.sessionSecret()); + +// Create QR-code with height and width of 570px and quiet area of 2 modules. +BufferedImage qrCodeBufferedImage = QrCodeGenerator.generateImage(deviceLink.toString(), 570, 570, 2); +// Return Data URI to frontend and display the QR-code +String qrCodeDataUri = QrCodeGenerator.convertToDataUri(qrCodeBufferedImage, "png"); +``` +### Validating callback URL + +When using same device flows (Web2App or App2App) the initialCallbackUrl will be used by the Smart-ID app to redirect the user back to the Relying Party application. +Received callback URL will contain additional query parameters that must be validated by the Relying Party. + +Example of received callback URL for authentication: +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc&userChallengeVerifier=XtPfaGa8JnGtYrJjboooUf0KfY9sMEHrWFpSQrsUv9c` + +Example of received callback URL for signature or certificate choice +`https://rp.example.com/callback-url?value=RrKjjT4aggzu27YBddX1bQ&sessionSecretDigest=U4CKK13H1XFiyBofev9asqrzIrY5_Gszi_nL_zDKkBc` + +1. RP must verify that the user sessions has `callbackUrl.urlToken()` with same value as in query parameter `value`. +2. RP must verify that the `sessionSecretDigest` query parameter matches the calculated digest created from session secret received in device link session init response. + For this library provides `CallbackUrlUtil.validateSessionSecretDigest(digestFromCallbackUrl, sessionSecret)` +3. For authentication same device flow RP also must verify the `userChallengeVerifier` query parameter. This can be done when polling the session status has finished and session status response has to be + validated. `deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, brokeredRpName);` + Value to validate `userChallengeVerifier` is in the session status response `signature.userChallenge`. + +## Session status request handling for v3.1 + +The Smart-ID v3.1 API includes new session status request path for retrieving session results. +Session status request is to be used for device link-based and notification-based flows. + +### Session status response + +The session status response includes various fields depending on whether the session has completed or is still running. Below are the key fields returned in the response: + +* `state`: RUNNING or COMPLETE +* `result.endResult`: Outcome of the session (e.g., OK, USER_REFUSED, TIMEOUT) +* `result.documentNumber`: Document number returned when `endResult` is `OK`. Can be used in further signature and authentication requests to target the same device. +* `result.details`: Contains additional info when user refused interaction +* `signatureProtocol`: Either ACSP_V2 (for authentication) or RAW_DIGEST_SIGNATURE (for signature) +* `signature`: Contains the following fields based on the signatureProtocol used: + * For `ACSP_V2`: value, serverRandom, userChallenge, flowType, signatureAlgorithm, signatureAlgorithmParameters, + * For `RAW_DIGEST_SIGNATURE`: value, flowType, signatureAlgorithm, signatureAlgorithmParameters +* `cert`: Includes certificate information with value (Base64-encoded certificate) and certificateLevel (ADVANCED or QUALIFIED). +* `ignoredProperties`: Any unsupported or ignored properties from the request. +* `interactionTypeUsed`: The interaction type used for the session. +* `deviceIpAddress`: IP address of the mobile device, if requested. + +### Examples of querying session status in v3.1 + +#### Example of using session status poller to query final sessions status + +The following example shows how to use the SessionStatusPoller to fetch the session status until it's complete. + +```java +*SessionResponse sessionResponse; +// Get the session status poller +SessionsStatusPoller poller = client.getSessionsStatusPoller(); + +// Get sessionID from current session response +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) +if("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + System.out.println("Session completed with result: "+sessionStatus.getResult().getEndResult()); +} +``` + +#### Example of querying sessions status only once +The following example shows how to use the SessionStatusPoller to only query the sessions status single time. +NB! If using this method for device link-based flows. Make sure the pollingSleepTimeout is not set or does not impact generating the QR-code for every second. + +```java +*SessionResponse sessionResponse; +// Get the session status poller +SessionStatusPoller poller = client.getSessionStatusPoller(); + +// Querying the sessions status +SessionStatus sessionStatus = poller.getSessionStatus(sessionResponse.sessionID()); +// Checking sessions state +if ("RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + // Session is still running and querying can be continued + // Dynamic content can be generated and displayed to the user +} else if ("COMPLETE".equalsIgnoreCase(sessionStatus.getState())){ + // continue to validate the sessions status +} else { + throw UnprocessableSmartIdResponseException("Invalid session state was returned"); +} +``` + +### Validating session status response + +It's important to validate the session status response to ensure that the returned signature or authentication result is valid. +For validating authentication session status response, use the `AuthenticationResponseValidator`. +For validating signature session status response, use the `SignatureResponseValidator`. +NB! Integrators must validate signature value against expected signature value. + +#### Set up CertificateValidator + +CertificateValidator will check if the certificate is not expired and is trusted +by constructing certificate chain with trust anchors and intermediate CA certificates provided in the TrustedCACertStore. +Will be used by AuthenticationResponseValidator and SignatureResponseValidator. + +```java +// Set up TrustedCACertStore +// Option 1 - initialize certificate store with default locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + +// Option 2 - initialize certificate store with custom locations for trust anchor truststore and for intermediate CA certificates +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder() + .withTrustAnchorTruststorePath("path/to/trustAnchorTruststore.jks") + .withTrustAnchorTruststorePassword("password") + .withIntermediateCAStorePath("path/to/intermediateCAStore.jks") + .withIntermediateCAStorePassword("password") + .build(); + + +// Option 3 - Provide trust anchors and intermediate CA certificates directly +Set trustAnchors; +List intermediateCACertificates; +TrustedCACertStore trustedCACertStore = new DefaultTrustedCACertStore() + .withTrustAnchors(trustAnchors) + .withIntermediateCACertificates(intermediateCACertificates) + .build(); + +// Set up CertificateValidator with the trusted CA store +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); +``` + +#### Example of validating the authentication sessions response: + +##### Device link-based authentication session status validation + +DeviceLinkAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +// Set up AuthenticationResponseValidator with the CertificateValidator +DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +DeviceLinkAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +DeviceLinkSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); + +// get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` + +##### Notification-based authentication session status validation + +NotificationAuthenticationResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +// Set up AuthenticationResponseValidator with the CertificateValidator +NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator = new AuthenticationResponseValidator(certificateValidator); + +// Create authentication request builder +NotificationAuthenticationSessionRequestBuilder authenticationRequestBuilder = smartIdClient.createDeviceLinkAuthentication()...; +// Initialize session +NotificationAuthenticationSessionResponse sessionResponse = authenticationRequestBuilder.initAuthenticationSession(); +// Get request used for starting the authentication session and use it later to validate sessions status response +NotificationAuthenticationSessionRequest authenticationSessionRequest = authenticationRequestBuilder.getAuthenticationSessionRequest(); + +// get sessions result +SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); +SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionResponse.sessionID()); + +// validate sessions state is completed +if("COMPLETE".equals(sessionStatus.getState())){ + // validate the session status response with authentication session request and return authentication identity + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); +} +``` + +#### Example of validating the certificate choice session response: + +CertificateChoiceResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +try { + // Set up CertificateChoiceResponseValidator with the CertificateValidator + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(sessionStatus); + +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); +} +``` + +#### Example of validating the signature session response: + +SignatureResponseValidator depends on CertificateValidator. Checkout [setting up CertificateValidator](#set-up-certificatevalidator) + +```java +try { + // Objects needed for validation + CertificateResponse certResponse; // queried by document number or use CertificateChoiceResponse + SignableData signableData; // data that was sent for signing + // Initialize the signature response validator with CertificateValidator + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + // Validate and map the session status. If the sessions end result is other than OK, then an exception will be thrown. + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED.name()); + // Validate signature value. This step can be skipped if other means of validating the signature value can be used. + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certResponse.certificate(), + signatureResponse.getRsaSsaPssParameters()); + + // Process the response (e.g., save to database or pass to another system) + handleSignatureResponse(signatureResponse); + +} catch (UserRefusedException e) { + System.out.println("User refused the session."); +} catch (SessionTimeoutException e) { + System.out.println("Session timed out."); +} catch (DocumentUnusableException e) { + System.out.println("Document is unusable for the session."); +} catch (SmartIdClientException e) { + System.out.println("An unexpected error occurred: " + e.getMessage()); +} +``` + +### Error handling for session status + +The session status response may return various error codes indicating the outcome of the session. Below are the possible end result values for a completed session: + +* `OK`: Session completed successfully. +* `USER_REFUSED`: User refused the session. +* `TIMEOUT`: User did not respond in time. +* `DOCUMENT_UNUSABLE`: Session could not be completed due to an issue with the document. +* `WRONG_VC`: User selected the wrong verification code. +* `REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP`: The requested interaction is not supported by the user's app. +* `USER_REFUSED_CERT_CHOICE`: User has multiple accounts and pressed Cancel on device choice screen. +* `USER_REFUSED_INTERACTION`: User pressed Cancel on the interaction screen. `interaction` field in the result details contains info which interaction + was canceled. + * `displayTextAndPIN` - User pressed Cancel on PIN screen during displayTextAndPIN flow. + * `confirmationMessage` - User cancelled on confirmationMessage screen. + * `confirmationMessageAndVerificationCodeChoice` - User cancelled on confirmationMessageAndVerificationCodeChoice screen. +* `PROTOCOL_FAILURE`: An error occurred in the signing protocol. +* `EXPECTED_LINKED_SESSION`: RP has configured signature session that should follow device-link certificate choice session incorrectly and the process + cannot be completed. +* `SERVER_ERROR` - Technical error occurred at the server side and the process was terminated. + +## Certificate by document number + +In API v3.1 new endpoint was introduced to simplify querying certificate for signing. +RP can directly query the user's signing certificate by document number — no session flow or user interaction required. +Can be used for device link and notification-based signature flows. +Only requirement is that the device link authentication is successfully completed before to get the document number. + +### Request Parameters +The request parameters for the certificate by document number request are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are `ADVANCED`, `QUALIFIED` or `QSCD`. Defaults to `QUALIFIED`. + +### Response Parameters +* `state`: Required. Indicates result. Possible values: + * `OK`: Certificate found and returned. + * `DOCUMENT_UNUSABLE`: user's Smart-ID account is not usable for signing +* `cert`: Required. Object containing the signing certificate. + * `value`: Required. Base64-encoded X.509 certificate (matches pattern `^[a-zA-Z0-9+/]+={0,2}$`) + * `certificateLevel`: Required. Level of the certificate, Possible values `ADVANCED` or `QUALIFIED` + +### Example of querying certificate by document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// Build the certificate by document number request and query the certificate +CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + +// Set up the certificate validator +TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); +CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + +// Validate the certificate +certificateValidator.validateCertificate(certResponse.certificate()); +``` +Checkout out other ways to set up TrustedCaCertStore with CertificateValidator in [Set up CertificateValidator](#set-up-certificatevalidator). + +## Linked signature flow + +In API v3.1 a new flow was introduced to link signature session to a previously completed certificate choice session. +The flow starts off with device link certificate choice session and must be followed by a linked notification-based signature session. + +### Device link certificate choice session + +Anonymous device link certificate choice session can be initiated without knowing the user's document number. When the session is completed successfully, +the Smart-ID API will stay waiting for the RP to start the [linked notification-based signature session](#linked-notification-based-signature-session). + +#### Request Parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. Used for overriding idempotent behaviour. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. +* `initialCallbackUrl` : Optional. Must match regex `^https:\/\/([^\\|]+)$`. If it contains the vertical bar `|`, it must be percent-encoded. Should be used for same-device flow. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. +* `sessionToken`: Unique random value that will be used to connect created session between the relevant parties (RP, RP-API, mobile app). +* `sessionSecret`: Base64-encoded random key value that should be kept secret and shared only between the RP backend and the RP-API server. +* `deviceLinkBase`: Required. Base URI used to form the device link or QR code. + +#### Example of initiating a device-link certificate choice session + +```java +DeviceLinkSessionResponse certificateChoice = client.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl("https://example.com/callback") // Only needed for same-device flows(Web2App, App2App) + .initiateCertificateChoice(); + +String sessionId = certificateChoice.sessionID(); +// SessionID is used to query sessions status later + +String sessionToken = certificateChoice.sessionToken(); +// Store sessionSecret only on backend side. Do not expose it to the client side. +String sessionSecret = certificateChoice.sessionSecret(); +String deviceLinkBase = certificateChoice.deviceLinkBase(); +Instant responseReceivedAt = certificateChoice.receivedAt(); +``` +Jump to [Generate QR-code and device link](#generating-qr-code-or-device-link) to see how to generate QR-code or device link from the response. +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Linked notification-based signature session + +Second part of the linked signature flow. Will be used to start the signature session after the device link certificate choice session is completed successfully. + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only supported value is `rsassa-pss` + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `linkedSessionID`: Required. Session ID of the previously completed certificate choice session. +* `interactions`: Required. Base64-encoded JSON string of an array of interaction objects. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. +* `requestProperties`: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response parameters + +* `sessionID`: Required. String that can be used to request the signature session status result. + +#### Example of initiating a linked notification-based signature session + +```java +// Prerequisite: device link certificate choice has been completed successfully. +DeviceLinkSessionResponse certificateChoiceSessionResponse; +CertificateChoiceResponse certificateChoiceResponse; + +// Start the linked notification signature session using the sessionID from the certificate choice session +LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionResponse.sessionID()) + .withSignableData(new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// SessionID is used to query sessions status later +String sessionId = signatureSessionResponse.sessionID(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +## Notification-based flows + +### Differences between notification-based, device link-based and linked flows + +* `Notification-Based flow` + * Push notifications: The user gets a notification directly on their Smart-ID app to proceed with the signing or authentication process. + * Known users or devices: + * Notification-based flows are more vulnerable to phishing attacks. It is recommended to use notification-based flows after the user has been identified by using device link-based flows. + * No dynamic updates: The process is straightforward, with no need to update links or use QR codes. +* `Device Link flow` + * Device links: Generates links for QR codes or Web2App/App2App links that the user interacts with to start the process. + * Anonymous authentication: the user's details are not required beforehand. RP validates the user after the Smart-ID authentication is completed. + * Real-time updates: QR-code needs to be refreshed every second to ensure validity. +* `Linked flow` + * Combination of anonymous certificate choice and notification-based signing: Starts with a device link-based certificate choice session followed by a notification-based signing session. + * QR-code or device link will be used only for the certificate choice part of the flow. + * Supports only device link-based interactions in the signature part of the flow. + +### Notification-based authentication session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is ACSP_V2. +* `signatureProtocolParameters`: Required. Parameters for the ACSP_V2 signature protocol. + * `rpChallenge`: Required. Random value with size in range of 32-64 bytes. Must be Base64 encoded. + * `signatureAlgorithm`: Required. Signature algorithm name. Supported values is 'rsassa-pss' + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm name. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. An array of interaction objects defining the interactions in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. +* `vcType`: Required. Type of verification code to be used. Currently, the only allowed value is `numeric4`. + +#### Response parameters +* `sessionID`: Required. String used to request the operation result. + +#### Examples of initiating a notification-based authentication session + +##### Initiating a notification-based authentication session with document number + +```java +String documentNumber = "PNOLT-40504040001-MOCK-Q"; + +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + .initAuthenticationSession(); + +// SessionID is used to query sessions status later +String sessionId = authenticationSessionResponse.sessionID(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based authentication session with semantics identifier + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// For security reasons a rpChallenge must be created for each new authentication request +RpChallenge rpChallenge = RpChallengeGenerator.generate(); +// Store generated rpChallenge only on backend side. Do not expose it to the client side. +// Used for validating authentication sessions status OK response + +// Generate verification code and display it to the user for confirmation +String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into "))) // Display text should be concise and specific. + .initAuthenticationSession(); + +// SessionID can be used to query sessions status later +String sessionId = authenticationSessionResponse.sessionID(); + +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Notification-based certificate choice session + +#### Request parameters + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. ADVANCED/QUALIFIED/QSCD, defaults to QUALIFIED. +* `nonce`: Random string, up to 30 characters. If present, must have at least 1 character. +* `capabilities`: Used only when agreed with Smart-ID provider. When omitted, request capabilities are derived from certificateLevel. +* `requestProperties`: A request properties object as a set of name/value pairs. For example, requesting the IP address of the user's device. + +#### Response parameters + +* `sessionID`: A string that can be used to request the session status result. + +#### Examples of initiating a notification-based certificate choice session + +##### Initiating a notification-based certificate choice session using semantics identifier + +```java +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + +// Use requested certificate level to validate certificate choice session status OK response. +CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" +NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = client + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(requestedCertificateLevel) + .initCertificateChoice(); + +String sessionId = certificateChoiceSessionResponse.sessionID(); +// SessionID is used to query sessions status later +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Notification-based signature session + +#### Request Parameters +The request parameters for the notification-based signature session are as follows: + +* `relyingPartyUUID`: Required. UUID of the Relying Party. +* `relyingPartyName`: Required. Friendly name of the Relying Party, limited to 32 bytes in UTF-8 encoding. +* `certificateLevel`: Level of certificate requested. Possible values are ADVANCED, QUALIFIED or QSCD. Defaults to QUALIFIED. +* `signatureProtocol`: Required. Signature protocol to use. Currently, the only allowed value is RAW_DIGEST_SIGNATURE. +* `signatureProtocolParameters`: Required. Parameters for the RAW_DIGEST_SIGNATURE signature protocol. + * `digest`: Required. Base64 encoded digest to be signed. + * `signatureAlgorithm`: Required. Signature algorithm name. Only `rsassa-pss` is currently supported. + * `signatureAlgorithmParameters`: Required. Parameters for the signature algorithm. + * `hashAlgorithm`: Required. Hash algorithm used for digest. Supported values are `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-256`, `SHA3-384`, `SHA3-512`. +* `interactions`: Required. Base64-encoded string of interactions to be used for a session. The interactions are defined in order of preference. + * Each interaction object includes: + * `type`: Required. Type of interaction. Allowed types are `displayTextAndPIN`, `confirmationMessage`, `confirmationMessageAndVerificationCodeChoice`. + * `displayText60` or `displayText200`: Required based on type. Text to display to the user. `displayText60` is limited to 60 characters, and `displayText200` is limited to 200 characters. +* `nonce`: Optional. Random string, up to 30 characters. If present, must have at least 1 character. To be used for overriding idempotency. +* `requestProperties`: requestProperties: + * `shareMdClientIpAddress`: Optional. Boolean indicating whether to request the IP address of the user's device. +* `capabilities`: Optional. Array of strings specifying capabilities. Used only when agreed with the Smart-ID provider. + +#### Response Parameters +* `sessionID`: Required. String used to request the operation result. +* `vc`: Required. Object describing the verification code details. + * `type`: Required. Type of the verification code. Currently, the only allowed type is `numeric4`. + * `value`: Required. Value of the verification code to be displayed to the user. + +#### Examples of initiating a notification-based signature session + +##### Initiating a notification-based signature session with semantics identifier + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Create the Semantics Identifier +SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" +); + +// Build the notification signature request +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the ")) // Display text should be concise and specific. + ) + .initSignatureSession(); + +// Get the session ID and continue to querying session status +String sessionID = signatureSessionResponse.sessionID(); + +// Display verification code to the user +String verificationCode = signatureSessionResponse.vc().getValue(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +##### Initiating a notification-based signature session with document number + +```java +// Create the signable data +var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + +// Specify the document number +String documentNumber = "PNOEE-40504040001-MOCK-Q"; + +// Initiate the session +NotificationSignatureSessionResponse signatureResponse = client.createNotificationSignature() + .withRelyingPartyUUID(client.getRelyingPartyUUID()) + .withRelyingPartyName(client.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withAllowedInteractionsOrder(List.of( + NotificationInteraction.confirmationMessage("Please sign the "))) // Display text should be concise and specific. + .initSignatureSession(); + +// Get the session ID and continue to querying session status +String sessionID = signatureResponse.sessionID(); + +// Display verification code to the user +String verificationCode = signatureResponse.vc().getValue(); +``` +Jump to [Query session status](#example-of-using-session-status-poller-to-query-final-sessions-status) for an example of session querying. + +### Error Handling + +Handle exceptions appropriately. The Java client provides specific exceptions for different error scenarios, such as: +* `UserAccountNotFoundException` +* `RelyingPartyAccountConfigurationException` +* `SessionNotFoundException` +* `RequiredInteractionNotSupportedByAppException` +* `ServerMaintenanceException` +* `SmartIdClientException` + +#### Example of Error Handling +```java +try { + NotificationSignatureSessionResponse response = builder.initSignatureSession(); +} catch (UserAccountNotFoundException e) { + System.out.println("User account not found."); +} catch (RelyingPartyAccountConfigurationException e) { + System.out.println("Relying party account configuration issue."); +} catch (RequiredInteractionNotSupportedByAppException e) { + System.out.println("The required interaction is not supported by the user's app."); +} catch (ServerMaintenanceException e) { + System.out.println("Server maintenance in progress, please try again later."); +} catch (SmartIdClientException e) { + System.out.println("An error occurred: " + e.getMessage()); +} +``` + +### Additional notification-based session request parameters + +#### Using nonce to override idempotent behaviour + +Idempotent behaviour means that if the session request with same values is made multiple times within a 15-second window, +the same response with identical values will be returned. If there is a need to override this behaviour, a nonce can be used. +Nonce value must be a random string with a minimum length of 1 and a maximum length of 30 characters. + +Notification-based signature request is used as an example. Nonce can also be used with other signing session request +(device-link signature and certificate choice; notification-based certificate choice) by using method `withNonce("randomValue")`. + +```java +NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withRelyingPartyUUID(smartIdClient.getRelyingPartyUUID()) + .withRelyingPartyName(smartIdClient.getRelyingPartyName()) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(Collections.singletonList( + NotificationInteraction.confirmationMessage("Please sign the ") // Display text should be concise and specific. + )) + // if request is made again in 15 seconds, the idempotent behaviour applies and same response with same values will be returned + // set nonce to override idempotent behaviour + .withNonce("randomValue") + .initSignatureSession(); +``` + +#### Using request properties to request the IP address of the user's device + +For the IP to be returned the service provider (SK) must switch on this option. +More info available at https://sk-eid.github.io/smart-id-documentation/rp-api/3.0.3/request_properties.html#ip_sharing + +Authentication is used for an example, shareMdClientIpAddress can also be used with certificate choice and signature sessions request by using method `withShareMdClientIpAddress(true)`. + +```java +NotificationAuthenticationSessionResponse authenticationSessionResponse = client + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Logging into ") // Display text should be concise and specific. + )) + // setting property to request the IP-address of the user's device + .withShareMdClientIpAddress(true) + .initAuthenticationSession(); +``` + +### Examples of notification-based interactions order + +An app can support different interaction types, and a Relying Party can specify the preferred interactions with or without fallback options. +Different interactions can support different amounts of data to display information to the user. + +Below are examples of `interactions`. + +Example 1: `confirmationMessageAndVerificationCodeChoice` with fallback to `confirmationMessage` and with fallback to `displayTextAndPIN` +Description: The RP's first choice is `confirmationMessageAndVerificationCodeChoice`; The second choice is `confirmationMessage`; The third choice is `displayTextAndPIN`. +```java +builder.withInteractions(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here..."), + NotificationInteraction.confirmationMessage("Up to 200 characters of text here..."), + NotificationInteraction.displayTextAndPin("Up to 60 characters of text here...") +)); +``` + +Example 2: `confirmationMessageAndVerificationCodeChoice` only +Description: Use `confirmationMessageAndVerificationCodeChoice` interaction exclusively. +NB! Process will fail when interaction is not supported and there is no fallback +```java +builder.withInteractions(List.of( + NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Up to 200 characters of text here...") +)); +``` + +## Exception Handling +The Smart-ID Java client library provides specific exceptions for different error scenarios. Handle exceptions appropriately to provide a good user experience. + +Exception Categories +* Permanent Exceptions + These exceptions indicate issues that are unlikely to be resolved by retrying the request. They are typically caused by client misconfiguration or invalid data input + * `SmartIdClientException` Thrown for general client-side errors, such as: + * Missing or invalid configuration (e.g., `trustSslContext` not set). + * `SmartIdRequestSetupException` Thrown when the request field validations fails, such as: + * Missing required fields (e.g., `relyingPartyUUID`, `relyingPartyName`, `signatureProtocol`). + * Invalid values for fields (e.g. `interactionType` containing duplicate types). +* Unprocessable Response Exceptions + These exceptions are thrown when the response from the Smart-ID service cannot be processed, typically due to malformed data or protocol violations. + * `UnprocessableSmartIdResponseException`: Thrown when the response from the Smart-ID service cannot be processed. + * Missing required fields (e.g., `state`, `endResult`, `signatureAlgorithm`). + * Incorrectly encoded Base64 strings in signature or certificate. + * Unexpected or unsupported `signatureProtocol`. +* User Action Exceptions + These exceptions cover scenarios where user actions or inactions lead to session termination or errors. + * `UserRefusedException` Base exception for user refusal scenarios. + * `SessionTimeoutException`: User did not respond within the allowed timeframe. + * `UserSelectedWrongVerificationCodeException` Thrown when the user selects an incorrect verification code during the process. +* User Account Exceptions + These exceptions handle issues related to the user's Smart-ID account or session requirements. + * `CertificateLevelMismatchException` Thrown when the returned certificate level does not meet the requested level. + * `DocumentUnusableException` Indicates that the requested document cannot be used for the operation. + * `UserAccountUnusableException` Thrown when the user's Smart-ID account is not currently usable for the requested operation. +* Validation and Parsing Exceptions + These exceptions arise during validation or parsing operations within the library. + * `CertificateParsingException` Thrown when the X.509 certificate cannot be parsed. + * `SignatureValidationException` Thrown when signature validation fails due to mismatched algorithms or corrupted data. +* Server side exceptions + * `ProtocolFailureException` Thrown when the Smart-ID API received invalid data such (f.e wrong data in generate device link) + * `ExpectedLinkedSessionException` Thrown when the Relying Party did not configure linked signature session to follow anonymous device-link certificate choice session. + * `SmartIdServerException` Thrown when the Smart-ID terminates the process due to a server-side error. + +## Network connection configuration of the client + +Under the hood each operation (authentication, choosing certificate and signing) consist of 2 request steps: + +- Initiation request +- Session status request + +Session status request by default is a long poll method, meaning the request method might not return until a timeout expires. Caller can tune each poll's timeout value in milliseconds inside the bounds set by service operator to turn it into a short poll. + +```java +SmartIdClient client = new SmartIdClient(); +// ... +// sets the timeout for each session status poll +client.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 5L); +// sets the pause between each session status poll +client.setPollingSleepTimeout(TimeUnit.SECONDS, 1L); +``` + +As Smart-ID Java client uses Jersey client for network communication underneath, we've exposed Jersey API for network connection configuration. + +Here's an example how to configure HTTP connector's custom socket timeouts for the Smart-ID client: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig(); +clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 5000); +clientConfig.property(ClientProperties.READ_TIMEOUT, 30000); + +client.setNetworkConnectionConfig(clientConfig); +``` +And here's an example how to use Apache Http Client with custom socket timeouts as the HTTP connector instead of the default HttpUrlConnection: + +```java +SmartIdClient client = new SmartIdClient(); +// ... +ClientConfig clientConfig = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); +RequestConfig reqConfig = RequestConfig.custom() + .setConnectTimeout(5000) + .setSocketTimeout(30000) + .setConnectionRequestTimeout(5000) + .build(); +clientConfig.property(ApacheClientProperties.REQUEST_CONFIG, reqConfig); + +client.setNetworkConnectionConfig(clientConfig); +``` + +Keep in mind that the HTTP connector timeout of waiting for data shouldn't normally be less than the timeout for session status poll. + +### Example of creating a client with configured ssl context on JBoss using JAXWS RS + + +```java +ResteasyClient resteasyClient = new ResteasyClientBuilder() + .sslContext(SmartIdClient.createSslContext(Arrays.asList( + "pem cert 1", "pem cert 2"))) + .build(); + +SmartIdClient client = new SmartIdClient(); +client.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); +client.setRelyingPartyName("DEMO"); +client.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); +client.setConfiguredClient(resteasyClient); ``` \ No newline at end of file diff --git a/mvnw b/mvnw index 7cbf37ab..5643201c 100755 --- a/mvnw +++ b/mvnw @@ -1,316 +1,316 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`\\unset -f command; \\command -v java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 23b7079a..8a15b7f3 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,188 +1,188 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index eb61823b..7e13153b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,300 +1,300 @@ - - - 4.0.0 - - ee.sk.smartid - smart-id-java-client - jar - 3.0-SNAPSHOT - - Smart-ID Java client - Smart-ID Java client is a Java library that can be used for easy integration of the Smart-ID solution to information systems or e-services - https://github.com/SK-EID/smart-id-java-client - - - MIT License - - - - scm:git:git@github.com:SK-EID/smart-id-java-client.git - scm:git:git@github.com:SK-EID/smart-id-java-client.git - https://github.com/SK-EID/smart-id-java-client.git - - - - - Juhan Aasaru - Nortal - https://www.nortal.com - - - Andreas Valdma - Nortal - https://www.nortal.com - - - - - SK ID Solutions AS - https://www.skidsolutions.eu/en - - - - UTF-8 - 17 - 17 - 2.17.2 - 2.17.2 - 3.1.8 - 6.2.10.Final - - - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${jersey.version} - - - org.glassfish.jersey.inject - jersey-hk2 - ${jersey.version} - - - - org.glassfish.jersey.connectors - jersey-apache-connector - ${jersey.version} - test - - - - org.glassfish.jaxb - jaxb-runtime - 4.0.5 - - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.annotations.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - - org.slf4j - slf4j-api - 2.0.16 - - - - jakarta.ws.rs - jakarta.ws.rs-api - 4.0.0 - - - - org.bouncycastle - bcprov-jdk18on - 1.78.1 - - - - com.google.zxing - core - 3.5.3 - - - com.google.zxing - javase - 3.5.3 - - - - org.junit.jupiter - junit-jupiter-api - 5.11.0 - test - - - org.junit.jupiter - junit-jupiter-params - 5.11.0 - test - - - org.hamcrest - hamcrest-library - 3.0 - test - - - ch.qos.logback - logback-classic - 1.5.8 - test - - - org.wiremock - wiremock - 3.9.1 - test - - - org.mockito - mockito-core - 5.13.0 - test - - - - org.jboss.resteasy - resteasy-client - ${resteasy.version} - test - - - - org.jboss.resteasy - resteasy-jackson2-provider - ${resteasy.version} - test - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.0 - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - - - prepare-agent - - - - report - test - - report - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - attach-sources - - jar-no-fork - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.10.0 - - - attach-javadocs - - jar - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.4.0 - - - create-license-list - generate-resources - - aggregate-add-third-party - - - - - - mit - SK ID Solutions AS - Smart ID sample Java client - 2018 - UTF-8 - - src/main/java - src/test/java - - ${project.basedir}/src/license - ${project.basedir}/src/license/third-party-file-template.ftl - ${project.basedir} - LICENSE.3RD-PARTY - - - - - org.owasp - dependency-check-maven - 12.1.6 - - true - false - - - - - check - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.6.4 - - false - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - true - - - - - - - - - + + + 4.0.0 + + ee.sk.smartid + smart-id-java-client + jar + 3.0-SNAPSHOT + + Smart-ID Java client + Smart-ID Java client is a Java library that can be used for easy integration of the Smart-ID solution to information systems or e-services + https://github.com/SK-EID/smart-id-java-client + + + MIT License + + + + scm:git:git@github.com:SK-EID/smart-id-java-client.git + scm:git:git@github.com:SK-EID/smart-id-java-client.git + https://github.com/SK-EID/smart-id-java-client.git + + + + + Juhan Aasaru + Nortal + https://www.nortal.com + + + Andreas Valdma + Nortal + https://www.nortal.com + + + + + SK ID Solutions AS + https://www.skidsolutions.eu/en + + + + UTF-8 + 17 + 17 + 2.17.2 + 2.17.2 + 3.1.8 + 6.2.10.Final + + + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey.version} + test + + + + org.glassfish.jaxb + jaxb-runtime + 4.0.5 + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.annotations.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + org.slf4j + slf4j-api + 2.0.16 + + + + jakarta.ws.rs + jakarta.ws.rs-api + 4.0.0 + + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + + com.google.zxing + core + 3.5.3 + + + com.google.zxing + javase + 3.5.3 + + + + org.junit.jupiter + junit-jupiter-api + 5.11.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.11.0 + test + + + org.hamcrest + hamcrest-library + 3.0 + test + + + ch.qos.logback + logback-classic + 1.5.8 + test + + + org.wiremock + wiremock + 3.9.1 + test + + + org.mockito + mockito-core + 5.13.0 + test + + + + org.jboss.resteasy + resteasy-client + ${resteasy.version} + test + + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.10.0 + + + attach-javadocs + + jar + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.4.0 + + + create-license-list + generate-resources + + aggregate-add-third-party + + + + + + mit + SK ID Solutions AS + Smart ID sample Java client + 2018 + UTF-8 + + src/main/java + src/test/java + + ${project.basedir}/src/license + ${project.basedir}/src/license/third-party-file-template.ftl + ${project.basedir} + LICENSE.3RD-PARTY + + + + + org.owasp + dependency-check-maven + 12.1.6 + + true + false + + + + + check + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.4 + + false + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + true + + + + + + + + + diff --git a/src/license/LICENSE.EPL-1.0 b/src/license/LICENSE.EPL-1.0 index 027eadb1..8f48f685 100644 --- a/src/license/LICENSE.EPL-1.0 +++ b/src/license/LICENSE.EPL-1.0 @@ -1,204 +1,204 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC -LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM -CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - a) in the case of the initial Contributor, the initial code and - documentation distributed under this Agreement, and - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - -where such changes and/or additions to the Program originate from and are -distributed by that particular Contributor. A Contribution 'originates' from a -Contributor if it was added to the Program by such Contributor itself or -anyone acting on such Contributor's behalf. Contributions do not include -additions to the Program which: (i) are separate modules of software -distributed in conjunction with the Program under their own license agreement, -and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are -necessarily infringed by the use or sale of its Contribution alone or when -combined with the Program. - -"Program" means the Contributions distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, -including all Contributors. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free copyright license to - reproduce, prepare derivative works of, publicly display, publicly - perform, distribute and sublicense the Contribution of such Contributor, - if any, and such derivative works, in source code and object code form. - - b) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free patent license under - Licensed Patents to make, use, sell, offer to sell, import and otherwise - transfer the Contribution of such Contributor, if any, in source code and - object code form. This patent license shall apply to the combination of - the Contribution and the Program if, at the time the Contribution is - added by the Contributor, such addition of the Contribution causes such - combination to be covered by the Licensed Patents. The patent license - shall not apply to any other combinations which include the Contribution. - No hardware per se is licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the patent - or other intellectual property rights of any other entity. Each - Contributor disclaims any liability to Recipient for claims brought by - any other entity based on infringement of intellectual property rights or - otherwise. As a condition to exercising the rights and licenses granted - hereunder, each Recipient hereby assumes sole responsibility to secure - any other intellectual property rights needed, if any. For example, if a - third party patent license is required to allow Recipient to distribute - the Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has sufficient - copyright rights in its Contribution, if any, to grant the copyright - license set forth in this Agreement. - -3. REQUIREMENTS -A Contributor may choose to distribute the Program in object code form under -its own license agreement, provided that: - - a) it complies with the terms and conditions of this Agreement; and - - b) its license agreement: - i) effectively disclaims on behalf of all Contributors all - warranties and conditions, express and implied, including warranties - or conditions of title and non-infringement, and implied warranties - or conditions of merchantability and fitness for a particular - purpose; - ii) effectively excludes on behalf of all Contributors all liability - for damages, including direct, indirect, special, incidental and - consequential damages, such as lost profits; - iii) states that any provisions which differ from this Agreement are - offered by that Contributor alone and not by any other party; and - iv) states that source code for the Program is available from such - Contributor, and informs licensees how to obtain it in a reasonable - manner on or through a medium customarily used for software - exchange. - -When the Program is made available in source code form: - - a) it must be made available under this Agreement; and - - b) a copy of this Agreement must be included with each copy of the - Program. -Contributors may not remove or alter any copyright notices contained within -the Program. - -Each Contributor must identify itself as the originator of its Contribution, -if any, in a manner that reasonably allows subsequent Recipients to identify -the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION -Commercial distributors of software may accept certain responsibilities with -respect to end users, business partners and the like. While this license is -intended to facilitate the commercial use of the Program, the Contributor who -includes the Program in a commercial product offering should do so in a manner -which does not create potential liability for other Contributors. Therefore, -if a Contributor includes the Program in a commercial product offering, such -Contributor ("Commercial Contributor") hereby agrees to defend and indemnify -every other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits and -other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such Commercial -Contributor in connection with its distribution of the Program in a commercial -product offering. The obligations in this section do not apply to any claims -or Losses relating to any actual or alleged intellectual property -infringement. In order to qualify, an Indemnified Contributor must: a) -promptly notify the Commercial Contributor in writing of such claim, and b) -allow the Commercial Contributor to control, and cooperate with the Commercial -Contributor in, the defense and any related settlement negotiations. The -Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product -offering, Product X. That Contributor is then a Commercial Contributor. If -that Commercial Contributor then makes performance claims, or offers -warranties related to Product X, those performance claims and warranties are -such Commercial Contributor's responsibility alone. Under this section, the -Commercial Contributor would have to defend claims against the other -Contributors related to those performance claims and warranties, and if a -court requires any other Contributor to pay any damages as a result, the -Commercial Contributor must pay those damages. - -5. NO WARRANTY -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, -NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each -Recipient is solely responsible for determining the appropriateness of using -and distributing the Program and assumes all risks associated with its -exercise of rights under this Agreement , including but not limited to the -risks and costs of program errors, compliance with applicable laws, damage to -or loss of data, programs or equipment, and unavailability or interruption of -operations. - -6. DISCLAIMER OF LIABILITY -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY -CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION -LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY -OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of the -remainder of the terms of this Agreement, and without further action by the -parties hereto, such provision shall be reformed to the minimum extent -necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Program itself -(excluding combinations of the Program with other software or hardware) -infringes such Recipient's patent(s), then such Recipient's rights granted -under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to -comply with any of the material terms or conditions of this Agreement and does -not cure such failure in a reasonable period of time after becoming aware of -such noncompliance. If all Recipient's rights under this Agreement terminate, -Recipient agrees to cease use and distribution of the Program as soon as -reasonably practicable. However, Recipient's obligations under this Agreement -and any licenses granted by Recipient relating to the Program shall continue -and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in -order to avoid inconsistency the Agreement is copyrighted and may only be -modified in the following manner. The Agreement Steward reserves the right to -publish new versions (including revisions) of this Agreement from time to -time. No one other than the Agreement Steward has the right to modify this -Agreement. The Eclipse Foundation is the initial Agreement Steward. The -Eclipse Foundation may assign the responsibility to serve as the Agreement -Steward to a suitable separate entity. Each new version of the Agreement will -be given a distinguishing version number. The Program (including -Contributions) may always be distributed subject to the version of the -Agreement under which it was received. In addition, after a new version of the -Agreement is published, Contributor may elect to distribute the Program -(including its Contributions) under the new version. Except as expressly -stated in Sections 2(a) and 2(b) above, Recipient receives no rights or -licenses to the intellectual property of any Contributor under this Agreement, -whether expressly, by implication, estoppel or otherwise. All rights in the -Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the -intellectual property laws of the United States of America. No party to this -Agreement will bring a legal action under this Agreement more than one year -after the cause of action arose. Each party waives its rights to a jury trial -in any resulting litigation. +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or +anyone acting on such Contributor's behalf. Contributions do not include +additions to the Program which: (i) are separate modules of software +distributed in conjunction with the Program under their own license agreement, +and (ii) are not derivative works of the Program. +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such Contributor, + if any, and such derivative works, in source code and object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and otherwise + transfer the Contribution of such Contributor, if any, in source code and + object code form. This patent license shall apply to the combination of + the Contribution and the Program if, at the time the Contribution is + added by the Contributor, such addition of the Contribution causes such + combination to be covered by the Licensed Patents. The patent license + shall not apply to any other combinations which include the Contribution. + No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the patent + or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights or + otherwise. As a condition to exercising the rights and licenses granted + hereunder, each Recipient hereby assumes sole responsibility to secure + any other intellectual property rights needed, if any. For example, if a + third party patent license is required to allow Recipient to distribute + the Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + +3. REQUIREMENTS +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + i) effectively disclaims on behalf of all Contributors all + warranties and conditions, express and implied, including warranties + or conditions of title and non-infringement, and implied warranties + or conditions of merchantability and fitness for a particular + purpose; + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software + exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. +Contributors may not remove or alter any copyright notices contained within +the Program. + +Each Contributor must identify itself as the originator of its Contribution, +if any, in a manner that reasonably allows subsequent Recipients to identify +the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, +if a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits and +other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such Commercial +Contributor in connection with its distribution of the Program in a commercial +product offering. The obligations in this section do not apply to any claims +or Losses relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: a) +promptly notify the Commercial Contributor in writing of such claim, and b) +allow the Commercial Contributor to control, and cooperate with the Commercial +Contributor in, the defense and any related settlement negotiations. The +Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If +that Commercial Contributor then makes performance claims, or offers +warranties related to Product X, those performance claims and warranties are +such Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a +court requires any other Contributor to pay any damages as a result, the +Commercial Contributor must pay those damages. + +5. NO WARRANTY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using +and distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement , including but not limited to the +risks and costs of program errors, compliance with applicable laws, damage to +or loss of data, programs or equipment, and unavailability or interruption of +operations. + +6. DISCLAIMER OF LIABILITY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION +LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of the +remainder of the terms of this Agreement, and without further action by the +parties hereto, such provision shall be reformed to the minimum extent +necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Program itself +(excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted +under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue +and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to +time. No one other than the Agreement Steward has the right to modify this +Agreement. The Eclipse Foundation is the initial Agreement Steward. The +Eclipse Foundation may assign the responsibility to serve as the Agreement +Steward to a suitable separate entity. Each new version of the Agreement will +be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the +Agreement under which it was received. In addition, after a new version of the +Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly +stated in Sections 2(a) and 2(b) above, Recipient receives no rights or +licenses to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in the +Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial +in any resulting litigation. diff --git a/src/license/third-party-file-template.ftl b/src/license/third-party-file-template.ftl index f55b4289..5e58e2ce 100644 --- a/src/license/third-party-file-template.ftl +++ b/src/license/third-party-file-template.ftl @@ -1,21 +1,21 @@ -<#function licenseFormat licenses> - <#assign result = ""/> - <#list licenses as license> - <#assign result = result + " (" +license + ")"/> - - <#return result> - -<#function artifactFormat p> - <#if p.name?index_of('Unnamed') > -1> - <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> - <#else> - <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> - - -List of ${dependencyMap?size} third-party dependencies (auto-generated on ${.now?date?iso_utc} with License Maven Plugin): - -<#list dependencyMap as e> -<#assign project = e.getKey()/> -<#assign licenses = e.getValue()/> -* ${licenseFormat(licenses)} ${artifactFormat(project)} - +<#function licenseFormat licenses> + <#assign result = ""/> + <#list licenses as license> + <#assign result = result + " (" +license + ")"/> + + <#return result> + +<#function artifactFormat p> + <#if p.name?index_of('Unnamed') > -1> + <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> + <#else> + <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - " + (p.url!"no url defined") + ")"> + + +List of ${dependencyMap?size} third-party dependencies (auto-generated on ${.now?date?iso_utc} with License Maven Plugin): + +<#list dependencyMap as e> +<#assign project = e.getKey()/> +<#assign licenses = e.getValue()/> +* ${licenseFormat(licenses)} ${artifactFormat(project)} + diff --git a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java index 31d1d721..f5f45506 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java +++ b/src/main/java/ee/sk/smartid/AuthenticationCertificateLevel.java @@ -1,71 +1,71 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents of authentication certificate levels. - */ -public enum AuthenticationCertificateLevel { - - /** - * Smart-ID basic certificate level. Use if you want to allow non-qualified and qualified accounts. - */ - ADVANCED(1), - /** - * Smart-ID highest certificate level. Use if you want to only allow qualified accounts. - */ - QUALIFIED(2); - - private final int level; - - AuthenticationCertificateLevel(int level) { - this.level = level; - } - - /** - * Check if current certificate level is same or higher than the given certificate level - * - * @param certificateLevel the level of the certificate - * @return the level of the certificate - */ - public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { - return this == certificateLevel || this.level > certificateLevel.level; - } - - /** - * Check if the given certificate level is supported - * - * @param certificateLevel the level of the certificate - * @return true if the level is supported, false otherwise - */ - public static boolean isSupported(String certificateLevel) { - return Arrays.stream(AuthenticationCertificateLevel.values()) - .anyMatch(cl -> cl.name().equals(certificateLevel)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents of authentication certificate levels. + */ +public enum AuthenticationCertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow non-qualified and qualified accounts. + */ + ADVANCED(1), + /** + * Smart-ID highest certificate level. Use if you want to only allow qualified accounts. + */ + QUALIFIED(2); + + private final int level; + + AuthenticationCertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return the level of the certificate + */ + public boolean isSameLevelOrHigher(AuthenticationCertificateLevel certificateLevel) { + return this == certificateLevel || this.level > certificateLevel.level; + } + + /** + * Check if the given certificate level is supported + * + * @param certificateLevel the level of the certificate + * @return true if the level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel) { + return Arrays.stream(AuthenticationCertificateLevel.values()) + .anyMatch(cl -> cl.name().equals(certificateLevel)); + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java index 3dfaad43..e2559dc5 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentity.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentity.java @@ -1,185 +1,185 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -/** - * Represents users identity in the validated authentication certificate - */ -public class AuthenticationIdentity { - - private String givenName; - private String surname; - private String identityNumber; - private String country; - private X509Certificate authCertificate; - private LocalDate dateOfBirth; - - /** - * Initializes a new instance of the authentication identity. - */ - public AuthenticationIdentity() { - } - - /** - * Initializes a new instance of authentication identity with the authentication certificate. - * - * @param authCertificate the authentication certificate where the identity information is extracted from - */ - public AuthenticationIdentity(X509Certificate authCertificate) { - this.authCertificate = authCertificate; - } - - /** - * Gets the given name of the user. - * - * @return the given name of the user - */ - public String getGivenName() { - return givenName; - } - - /** - * Sets the given name of the user. - * - * @param givenName the given name of the user - */ - public void setGivenName(String givenName) { - this.givenName = givenName; - } - - /** - * Gets the surname of the user. - * - * @return the surname of the user - */ - public String getSurname() { - return surname; - } - - /** - * Sets the surname of the user. - * - * @param surname the surname of the user - */ - public void setSurname(String surname) { - this.surname = surname; - } - - /** - * Gets the identity number of the user. - * - * @return the identity number of the user - */ - public String getIdentityNumber() { - return identityNumber; - } - - /** - * Sets the identity number of the user. - *

- * The identity number is also known as national identification number, personal code, social security number etc. - *

- * Should be used if the value are only the numbers. F.e. 12345678901 - * - * @param identityNumber the identity number of the user - */ - public void setIdentityNumber(String identityNumber) { - this.identityNumber = identityNumber; - } - - /** - * Gets the identity number of the user. - * - * @return the identity code of the user - */ - public String getIdentityCode() { - return identityNumber; - } - - /** - * Sets the identity number of the user. - *

- * The identity number is also known as national identification number, personal code, social security number etc. - *

- * Should be used if the value contains alphanumeric characters. F.e. EE12345678901, 1234567-8901 - * - * @param identityCode the identity code of the user - */ - public void setIdentityCode(String identityCode) { - this.identityNumber = identityCode; - } - - /** - * Gets the country code of the user. - * - * @return the country code of the user - */ - public String getCountry() { - return country; - } - - /** - * Sets the country code of the user. - * - * @param country the country code of the user - */ - public void setCountry(String country) { - this.country = country; - } - - /** - * Gets the authentication certificate of the user. - * - * @return the authentication certificate of the user - */ - public X509Certificate getAuthCertificate() { - return authCertificate; - } - - /** - * Person's date of birth. - * - * @return Date of birth if this information is available in authentication response or empty optional. - */ - public Optional getDateOfBirth() { - return Optional.ofNullable(dateOfBirth); - } - - /** - * Sets person's date of birth. - * - * @param dateOfBirth Date of birth - */ - public void setDateOfBirth(LocalDate dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +/** + * Represents users identity in the validated authentication certificate + */ +public class AuthenticationIdentity { + + private String givenName; + private String surname; + private String identityNumber; + private String country; + private X509Certificate authCertificate; + private LocalDate dateOfBirth; + + /** + * Initializes a new instance of the authentication identity. + */ + public AuthenticationIdentity() { + } + + /** + * Initializes a new instance of authentication identity with the authentication certificate. + * + * @param authCertificate the authentication certificate where the identity information is extracted from + */ + public AuthenticationIdentity(X509Certificate authCertificate) { + this.authCertificate = authCertificate; + } + + /** + * Gets the given name of the user. + * + * @return the given name of the user + */ + public String getGivenName() { + return givenName; + } + + /** + * Sets the given name of the user. + * + * @param givenName the given name of the user + */ + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + /** + * Gets the surname of the user. + * + * @return the surname of the user + */ + public String getSurname() { + return surname; + } + + /** + * Sets the surname of the user. + * + * @param surname the surname of the user + */ + public void setSurname(String surname) { + this.surname = surname; + } + + /** + * Gets the identity number of the user. + * + * @return the identity number of the user + */ + public String getIdentityNumber() { + return identityNumber; + } + + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value are only the numbers. F.e. 12345678901 + * + * @param identityNumber the identity number of the user + */ + public void setIdentityNumber(String identityNumber) { + this.identityNumber = identityNumber; + } + + /** + * Gets the identity number of the user. + * + * @return the identity code of the user + */ + public String getIdentityCode() { + return identityNumber; + } + + /** + * Sets the identity number of the user. + *

+ * The identity number is also known as national identification number, personal code, social security number etc. + *

+ * Should be used if the value contains alphanumeric characters. F.e. EE12345678901, 1234567-8901 + * + * @param identityCode the identity code of the user + */ + public void setIdentityCode(String identityCode) { + this.identityNumber = identityCode; + } + + /** + * Gets the country code of the user. + * + * @return the country code of the user + */ + public String getCountry() { + return country; + } + + /** + * Sets the country code of the user. + * + * @param country the country code of the user + */ + public void setCountry(String country) { + this.country = country; + } + + /** + * Gets the authentication certificate of the user. + * + * @return the authentication certificate of the user + */ + public X509Certificate getAuthCertificate() { + return authCertificate; + } + + /** + * Person's date of birth. + * + * @return Date of birth if this information is available in authentication response or empty optional. + */ + public Optional getDateOfBirth() { + return Optional.ofNullable(dateOfBirth); + } + + /** + * Sets person's date of birth. + * + * @param dateOfBirth Date of birth + */ + public void setDateOfBirth(LocalDate dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } + +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java index abe3fb5f..bff9b6d6 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationIdentityMapper.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -import org.bouncycastle.asn1.x500.style.BCStyle; - -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.NationalIdentityNumberUtil; - -/** - * Maps X509 certificate to an {@link AuthenticationIdentity} object. - */ -public final class AuthenticationIdentityMapper { - - private AuthenticationIdentityMapper() { - } - - /** - * Maps the X509 certificate to an {@link AuthenticationIdentity} object. - * - * @param certificate Certificate to be converted to an {@link AuthenticationIdentity} object - * @return AuthenticationIdentity object - */ - public static AuthenticationIdentity from(X509Certificate certificate) { - var identity = new AuthenticationIdentity(certificate); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) - .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); - CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); - identity.setDateOfBirth(getDateOfBirth(identity)); - return identity; - } - - private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { - return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) - .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.bouncycastle.asn1.x500.style.BCStyle; + +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.NationalIdentityNumberUtil; + +/** + * Maps X509 certificate to an {@link AuthenticationIdentity} object. + */ +public final class AuthenticationIdentityMapper { + + private AuthenticationIdentityMapper() { + } + + /** + * Maps the X509 certificate to an {@link AuthenticationIdentity} object. + * + * @param certificate Certificate to be converted to an {@link AuthenticationIdentity} object + * @return AuthenticationIdentity object + */ + public static AuthenticationIdentity from(X509Certificate certificate) { + var identity = new AuthenticationIdentity(certificate); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GIVENNAME).ifPresent(identity::setGivenName); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SURNAME).ifPresent(identity::setSurname); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.SERIALNUMBER) + .ifPresent(serialNumber -> identity.setIdentityNumber(serialNumber.split("-", 2)[1])); + CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.C).ifPresent(identity::setCountry); + identity.setDateOfBirth(getDateOfBirth(identity)); + return identity; + } + + private static LocalDate getDateOfBirth(AuthenticationIdentity identity) { + return Optional.ofNullable(CertificateAttributeUtil.getDateOfBirth(identity.getAuthCertificate())) + .orElse(NationalIdentityNumberUtil.getDateOfBirth(identity)); + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponse.java b/src/main/java/ee/sk/smartid/AuthenticationResponse.java index 7dc57cd9..25bdabba 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponse.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponse.java @@ -1,267 +1,267 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.Base64; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * The authentication response after a successful authentication session status response was received. - *

- * Used with {@link DeviceLinkAuthenticationResponseValidator} to validate the certificate used for authentication - * and the signature in the authentication response. - */ -public class AuthenticationResponse { - - private String endResult; - private String serverRandom; - private String userChallenge; - private String signatureValueInBase64; - private X509Certificate certificate; - private AuthenticationCertificateLevel certificateLevel; - private String documentNumber; - private String interactionTypeUsed; - private FlowType flowType; - private String deviceIpAddress; - private RsaSsaPssParameters rsaSsaPssSignatureParameters; - - /** - * Gets the end result of the authentication session. - * - * @return the end result of the authentication session - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the authentication session. - * - * @param endResult the end result of the authentication session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the signature value in Base64 encoding. - * - * @return signature value in Base64 encoding - */ - public String getSignatureValueInBase64() { - return signatureValueInBase64; - } - - /** - * Sets the signature value in Base64 encoding. - * - * @param signatureValueInBase64 signature value in Base64 encoding - */ - public void setSignatureValueInBase64(String signatureValueInBase64) { - this.signatureValueInBase64 = signatureValueInBase64; - } - - /** - * Decodes Base64 encoded signature value and returns it as a byte array. - * - * @return signature value as a byte array - */ - public byte[] getSignatureValue() { - try { - return Base64.getDecoder().decode(signatureValueInBase64.getBytes(StandardCharsets.UTF_8)); - } catch (IllegalArgumentException e) { - throw new UnprocessableSmartIdResponseException( - "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); - } - } - - /** - * Get the certificate used in authentication. - * - * @return the X509Certificate used in authentication - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate used in authentication. - * - * @param certificate the X509Certificate used in authentication - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the level of the authentication certificate. - * - * @return the level of the authentication certificate - */ - public AuthenticationCertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the level of the authentication certificate. - * - * @param certificateLevel the authentication certificate level in the session status response - */ - public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the document number used for authentication - * - * @return the document number - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number used for authentication - * - * @param documentNumber the document number from the session status response - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - /** - * Gets the interaction type used in authentication - * - * @return the interaction type used in authentication - */ - public String getInteractionTypeUsed() { - return interactionTypeUsed; - } - - /** - * Sets the interaction type used in authentication - * - * @param interactionTypeUsed the interaction type used in authentication - */ - public void setInteractionTypeUsed(String interactionTypeUsed) { - this.interactionTypeUsed = interactionTypeUsed; - } - - /** - * Gets the IP address of the device used in authentication - * - * @return the IP address of the device - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in authentication - * - * @param deviceIpAddress the IP address of the device - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - - /** - * Gets the server random in Base64 encoding - * - * @return server random - */ - public String getServerRandom() { - return serverRandom; - } - - /** - * Sets the server random in Base64 encoding - * - * @param serverRandom the server random from the session status response - */ - public void setServerRandom(String serverRandom) { - this.serverRandom = serverRandom; - } - - /** - * Gets the user challenge - * - * @return user challenge - */ - public String getUserChallenge() { - return userChallenge; - } - - /** - * Sets the user challenge - * - * @param userChallenge the user challenge from the session status response - */ - public void setUserChallenge(String userChallenge) { - this.userChallenge = userChallenge; - } - - /** - * Gets the flow type user used to complete the authentication - *

- * - * @return flow type - */ - public FlowType getFlowType() { - return flowType; - } - - /** - * Sets the flow type used in authentication - * - * @param flowType the flow type used in authentication - */ - public void setFlowType(FlowType flowType) { - this.flowType = flowType; - } - - /** - * Gets the RSASSA-PSS parameters - * - * @return return RSASSA-PSS parameters - */ - public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { - return rsaSsaPssSignatureParameters; - } - - /** - * Sets the RSASSA-PSS parameters - * - * @param rsaSsaPssSignatureParameters the RSASSA-PSS parameters from the session status response - */ - public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { - this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * The authentication response after a successful authentication session status response was received. + *

+ * Used with {@link DeviceLinkAuthenticationResponseValidator} to validate the certificate used for authentication + * and the signature in the authentication response. + */ +public class AuthenticationResponse { + + private String endResult; + private String serverRandom; + private String userChallenge; + private String signatureValueInBase64; + private X509Certificate certificate; + private AuthenticationCertificateLevel certificateLevel; + private String documentNumber; + private String interactionTypeUsed; + private FlowType flowType; + private String deviceIpAddress; + private RsaSsaPssParameters rsaSsaPssSignatureParameters; + + /** + * Gets the end result of the authentication session. + * + * @return the end result of the authentication session + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the authentication session. + * + * @param endResult the end result of the authentication session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the signature value in Base64 encoding. + * + * @return signature value in Base64 encoding + */ + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + /** + * Sets the signature value in Base64 encoding. + * + * @param signatureValueInBase64 signature value in Base64 encoding + */ + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + /** + * Decodes Base64 encoded signature value and returns it as a byte array. + * + * @return signature value as a byte array + */ + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64.getBytes(StandardCharsets.UTF_8)); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + /** + * Get the certificate used in authentication. + * + * @return the X509Certificate used in authentication + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate used in authentication. + * + * @param certificate the X509Certificate used in authentication + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the level of the authentication certificate. + * + * @return the level of the authentication certificate + */ + public AuthenticationCertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the level of the authentication certificate. + * + * @param certificateLevel the authentication certificate level in the session status response + */ + public void setCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the document number used for authentication + * + * @return the document number + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number used for authentication + * + * @param documentNumber the document number from the session status response + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + /** + * Gets the interaction type used in authentication + * + * @return the interaction type used in authentication + */ + public String getInteractionTypeUsed() { + return interactionTypeUsed; + } + + /** + * Sets the interaction type used in authentication + * + * @param interactionTypeUsed the interaction type used in authentication + */ + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; + } + + /** + * Gets the IP address of the device used in authentication + * + * @return the IP address of the device + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in authentication + * + * @param deviceIpAddress the IP address of the device + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + + /** + * Gets the server random in Base64 encoding + * + * @return server random + */ + public String getServerRandom() { + return serverRandom; + } + + /** + * Sets the server random in Base64 encoding + * + * @param serverRandom the server random from the session status response + */ + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } + + /** + * Gets the user challenge + * + * @return user challenge + */ + public String getUserChallenge() { + return userChallenge; + } + + /** + * Sets the user challenge + * + * @param userChallenge the user challenge from the session status response + */ + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + /** + * Gets the flow type user used to complete the authentication + *

+ * + * @return flow type + */ + public FlowType getFlowType() { + return flowType; + } + + /** + * Sets the flow type used in authentication + * + * @param flowType the flow type used in authentication + */ + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + + /** + * Gets the RSASSA-PSS parameters + * + * @return return RSASSA-PSS parameters + */ + public RsaSsaPssParameters getRsaSsaPssSignatureParameters() { + return rsaSsaPssSignatureParameters; + } + + /** + * Sets the RSASSA-PSS parameters + * + * @param rsaSsaPssSignatureParameters the RSASSA-PSS parameters from the session status response + */ + public void setRsaSsaPssSignatureParameters(RsaSsaPssParameters rsaSsaPssSignatureParameters) { + this.rsaSsaPssSignatureParameters = rsaSsaPssSignatureParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java index 9fa59666..eb40e98c 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapper.java @@ -1,47 +1,47 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * Represents a mapper for converting a SessionStatus to an AuthenticationResponse. - *

- * Used to map the received session status to an authentication response object. - *

- * Implementers should ensure that all mandatory fields are present. - */ -public interface AuthenticationResponseMapper { - - /** - * Validates the presence of mandatory fields and maps a SessionStatus to an AuthenticationResponse. - * - * @param sessionStatus the SessionStatus to map - * @return the mapped AuthenticationResponse - */ - AuthenticationResponse from(SessionStatus sessionStatus); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * Represents a mapper for converting a SessionStatus to an AuthenticationResponse. + *

+ * Used to map the received session status to an authentication response object. + *

+ * Implementers should ensure that all mandatory fields are present. + */ +public interface AuthenticationResponseMapper { + + /** + * Validates the presence of mandatory fields and maps a SessionStatus to an AuthenticationResponse. + * + * @param sessionStatus the SessionStatus to map + * @return the mapped AuthenticationResponse + */ + AuthenticationResponse from(SessionStatus sessionStatus); +} diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 156c19ac..75639014 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -1,278 +1,278 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Optional; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates and maps the received session status to authentication response - */ -public class AuthenticationResponseMapperImpl implements AuthenticationResponseMapper { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); - - private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; - private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; - private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; - - /** - * Maps session status to authentication response {@link AuthenticationResponse} - * - * @param sessionStatus session status received from Smart-ID server - * @return authentication response - */ - @Override - public AuthenticationResponse from(SessionStatus sessionStatus) { - validateSessionStatus(sessionStatus); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate sessionCertificate = sessionStatus.getCert(); - - var authenticationResponse = new AuthenticationResponse(); - authenticationResponse.setEndResult(sessionResult.getEndResult()); - authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); - authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); - authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); - authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); - - var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - var rssSsaPssParameters = new RsaSsaPssParameters(); - rssSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); - rssSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm())); - rssSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); - rssSsaPssParameters.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - rssSsaPssParameters.setTrailerField(TrailerField.fromString(signatureAlgorithmParameters.getTrailerField())); - authenticationResponse.setRsaSsaPssSignatureParameters(rssSsaPssParameters); - - authenticationResponse.setCertificate(toCertificate(sessionCertificate)); - authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); - authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); - authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return authenticationResponse; - } - - private static void validateSessionStatus(SessionStatus sessionStatus) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionsStatus' is not provided"); - } - - validateResult(sessionStatus.getResult()); - validateSignatureProtocol(sessionStatus); - validateSignature(sessionStatus.getSignature()); - validateCertificate(sessionStatus.getCert()); - - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'interactionTypeUsed' is empty"); - } - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result' is empty"); - } - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.endResult' is empty"); - } - if (!"OK".equals(endResult)) { - ErrorResultHandler.handle(sessionResult); - } - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.documentNumber' is empty"); - } - } - - private static void validateSignatureProtocol(SessionStatus sessionStatus) { - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' is empty"); - } - - if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { - logger.error("Authentication session status field 'signatureProtocol' has invalid value: {}", sessionStatus.getSignatureProtocol()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' has unsupported value"); - } - } - - private static void validateSignature(SessionSignature sessionSignature) { - if (sessionSignature == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature' is missing"); - } - - if (StringUtil.isEmpty(sessionSignature.getValue())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' is empty"); - } - if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { - logger.error("Authentication session status field 'signature.value' does not have Base64-encoded value: {}", sessionSignature.getValue()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' does not have Base64-encoded value"); - } - - if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' is empty"); - } - int serverRandomLength = sessionSignature.getServerRandom().length(); - if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { - logger.error("Authentication session status field 'signature.serverRandom' is less than required length. Expected: {}; Actual: {}", MINIMUM_SERVER_RANDOM_LENGTH, serverRandomLength); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' value length is less than required"); - } - if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { - logger.error("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value: {}", sessionSignature.getServerRandom()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value"); - } - - if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' is empty"); - } - if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { - logger.error("Authentication session status field 'signature.userChallenge' does not match required pattern. Expected pattern {}; actual value {}", USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' value does not match required pattern"); - } - - if (StringUtil.isEmpty(sessionSignature.getFlowType())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' is empty"); - } - if (!FlowType.isSupported(sessionSignature.getFlowType())) { - logger.error("Authentication session status field 'signature.flowType' has invalid value: {}", sessionSignature.getFlowType()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' has unsupported value"); - } - - if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' is empty"); - } - if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { - logger.error("Authentication session status field 'signature.signatureAlgorithm' has invalid value: {}", sessionSignature.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' has unsupported value"); - } - - validateSignatureAlgorithmParameters(sessionSignature); - } - - private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { - var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - if (signatureAlgorithmParameters == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); - } - if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); - } - - Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); - if (hashAlgorithm.isEmpty()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", signatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); - } - - var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); - if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); - } - if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); - } - if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value"); - } - - if (maskGenAlgorithm.getParameters() == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); - } - if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); - } - Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); - if (maskGenHashAlgorithm.isEmpty()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); - } - if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", - hashAlgorithm.get().getAlgorithmName(), - maskGenHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); - } - - if (signatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty"); - } - int octetLength = hashAlgorithm.get().getOctetLength(); - if (octetLength != signatureAlgorithmParameters.getSaltLength()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: {}, actual: {}", - octetLength, - signatureAlgorithmParameters.getSaltLength()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); - } - - if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); - } - if (!TrailerField.BC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); - } - } - - private static void validateCertificate(SessionCertificate sessionCertificate) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert' is missing"); - } - - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.value' is empty"); - } - - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); - } - if (!AuthenticationCertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Authentication session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' has unsupported value"); - } - } - - private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { - return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - } - - private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { - return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to authentication response + */ +public class AuthenticationResponseMapperImpl implements AuthenticationResponseMapper { + + private static final Logger logger = LoggerFactory.getLogger(AuthenticationResponseMapperImpl.class); + + private static final String USER_CHALLENGE_PATTERN = "^[a-zA-Z0-9-_]{43}$"; + private static final String BASE64_FORMAT_PATTERN = "^[a-zA-Z0-9+/]+={0,2}$"; + private static final int MINIMUM_SERVER_RANDOM_LENGTH = 24; + + /** + * Maps session status to authentication response {@link AuthenticationResponse} + * + * @param sessionStatus session status received from Smart-ID server + * @return authentication response + */ + @Override + public AuthenticationResponse from(SessionStatus sessionStatus) { + validateSessionStatus(sessionStatus); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + + var authenticationResponse = new AuthenticationResponse(); + authenticationResponse.setEndResult(sessionResult.getEndResult()); + authenticationResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + authenticationResponse.setServerRandom(sessionSignature.getServerRandom()); + authenticationResponse.setUserChallenge(sessionSignature.getUserChallenge()); + authenticationResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); + authenticationResponse.setSignatureValueInBase64(sessionSignature.getValue()); + + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var rssSsaPssParameters = new RsaSsaPssParameters(); + rssSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getAlgorithm())); + rssSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rssSsaPssParameters.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rssSsaPssParameters.setTrailerField(TrailerField.fromString(signatureAlgorithmParameters.getTrailerField())); + authenticationResponse.setRsaSsaPssSignatureParameters(rssSsaPssParameters); + + authenticationResponse.setCertificate(toCertificate(sessionCertificate)); + authenticationResponse.setCertificateLevel(toAuthenticationCertificateLevel(sessionCertificate)); + authenticationResponse.setInteractionTypeUsed(sessionStatus.getInteractionTypeUsed()); + authenticationResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return authenticationResponse; + } + + private static void validateSessionStatus(SessionStatus sessionStatus) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionsStatus' is not provided"); + } + + validateResult(sessionStatus.getResult()); + validateSignatureProtocol(sessionStatus); + validateSignature(sessionStatus.getSignature()); + validateCertificate(sessionStatus.getCert()); + + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'interactionTypeUsed' is empty"); + } + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result' is empty"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.endResult' is empty"); + } + if (!"OK".equals(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'result.documentNumber' is empty"); + } + } + + private static void validateSignatureProtocol(SessionStatus sessionStatus) { + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' is empty"); + } + + if (!SignatureProtocol.ACSP_V2.name().equals(sessionStatus.getSignatureProtocol())) { + logger.error("Authentication session status field 'signatureProtocol' has invalid value: {}", sessionStatus.getSignatureProtocol()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signatureProtocol' has unsupported value"); + } + } + + private static void validateSignature(SessionSignature sessionSignature) { + if (sessionSignature == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature' is missing"); + } + + if (StringUtil.isEmpty(sessionSignature.getValue())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' is empty"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getValue())) { + logger.error("Authentication session status field 'signature.value' does not have Base64-encoded value: {}", sessionSignature.getValue()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.value' does not have Base64-encoded value"); + } + + if (StringUtil.isEmpty(sessionSignature.getServerRandom())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' is empty"); + } + int serverRandomLength = sessionSignature.getServerRandom().length(); + if (serverRandomLength < MINIMUM_SERVER_RANDOM_LENGTH) { + logger.error("Authentication session status field 'signature.serverRandom' is less than required length. Expected: {}; Actual: {}", MINIMUM_SERVER_RANDOM_LENGTH, serverRandomLength); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' value length is less than required"); + } + if (!Pattern.matches(BASE64_FORMAT_PATTERN, sessionSignature.getServerRandom())) { + logger.error("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value: {}", sessionSignature.getServerRandom()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value"); + } + + if (StringUtil.isEmpty(sessionSignature.getUserChallenge())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' is empty"); + } + if (!Pattern.matches(USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge())) { + logger.error("Authentication session status field 'signature.userChallenge' does not match required pattern. Expected pattern {}; actual value {}", USER_CHALLENGE_PATTERN, sessionSignature.getUserChallenge()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.userChallenge' value does not match required pattern"); + } + + if (StringUtil.isEmpty(sessionSignature.getFlowType())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' is empty"); + } + if (!FlowType.isSupported(sessionSignature.getFlowType())) { + logger.error("Authentication session status field 'signature.flowType' has invalid value: {}", sessionSignature.getFlowType()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.flowType' has unsupported value"); + } + + if (StringUtil.isEmpty(sessionSignature.getSignatureAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' is empty"); + } + if (!SignatureAlgorithm.isSupported(sessionSignature.getSignatureAlgorithm())) { + logger.error("Authentication session status field 'signature.signatureAlgorithm' has invalid value: {}", sessionSignature.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithm' has unsupported value"); + } + + validateSignatureAlgorithmParameters(sessionSignature); + } + + private static void validateSignatureAlgorithmParameters(SessionSignature sessionSignature) { + var signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + if (signatureAlgorithmParameters == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters' is missing"); + } + if (StringUtil.isEmpty(signatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", signatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); + } + + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + } + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value"); + } + + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); + } + Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (maskGenHashAlgorithm.isEmpty()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); + } + if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", + hashAlgorithm.get().getAlgorithmName(), + maskGenHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + } + + if (signatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty"); + } + int octetLength = hashAlgorithm.get().getOctetLength(); + if (octetLength != signatureAlgorithmParameters.getSaltLength()) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: {}, actual: {}", + octetLength, + signatureAlgorithmParameters.getSaltLength()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); + } + + if (StringUtil.isEmpty(signatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty"); + } + if (!TrailerField.BC.getValue().equals(signatureAlgorithmParameters.getTrailerField())) { + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: {}", signatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value"); + } + } + + private static void validateCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert' is missing"); + } + + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.value' is empty"); + } + + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' is empty"); + } + if (!AuthenticationCertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Authentication session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'cert.certificateLevel' has unsupported value"); + } + } + + private static X509Certificate toCertificate(SessionCertificate sessionCertificate) { + return CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + } + + private static AuthenticationCertificateLevel toAuthenticationCertificateLevel(SessionCertificate sessionCertificate) { + return AuthenticationCertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java index 22709c90..ba61db1c 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilder.java @@ -1,190 +1,190 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for constructing request to query certificate from Smart-ID API - */ -public class CertificateByDocumentNumberRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); - - private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); - - private final SmartIdConnector connector; - - private String documentNumber; - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel = CertificateLevel.QUALIFIED; - - /** - * Constructs a new CertificateByDocumentNumberRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public CertificateByDocumentNumberRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the document number for the request. - * - * @param documentNumber the document number - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the relying party UUID for the request. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name for the request. - * - * @param relyingPartyName the relying party name - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level for the request. - * - * @param certificateLevel the certificate level - * @return this builder instance - */ - public CertificateByDocumentNumberRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Builds the request and retrieves the certificate by document number. - * - * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate - * @throws SmartIdClientException if any required parameters are missing or invalid - * @throws UnprocessableSmartIdResponseException if the response is not valid - * @throws DocumentUnusableException if the document is unusable - */ - public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { - validateRequestParameters(); - var request = new CertificateByDocumentNumberRequest(relyingPartyUUID, relyingPartyName, certificateLevel == null ? null : certificateLevel.name()); - CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); - validateResponseParameters(response); - - return new CertificateByDocumentNumberResult( - CertificateLevel.valueOf(response.cert().certificateLevel()), - CertificateParser.parseX509Certificate(response.cert().value())); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(documentNumber)) { - throw new SmartIdClientException("Value for 'documentNumber' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdClientException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdClientException("Value for 'relyingPartyName' cannot be empty"); - } - } - - private void validateResponseParameters(CertificateResponse certificateResponse) { - if (certificateResponse == null) { - throw new UnprocessableSmartIdResponseException("Queried certificate response is not provided"); - } - validateState(certificateResponse); - - if (certificateResponse.cert() == null) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); - } - validateCertificateLevel(certificateResponse); - - if (StringUtil.isEmpty(certificateResponse.cert().value())) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' is missing"); - } - if (!BASE64_PATTERN.matcher(certificateResponse.cert().value()).matches()) { - logger.error("Certificate response field 'cert.value' has invalid value: {}", certificateResponse.cert().value()); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' does not have Base64-encoded value"); - } - } - - private static void validateState(CertificateResponse certificateResponse) { - String state = certificateResponse.state(); - if (StringUtil.isEmpty(state)) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); - } - if (!CertificateState.isSupported(state)) { - logger.error("Queried certificate response field 'state' has invalid value: {}", state); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' has unsupported value"); - } - if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { - throw new DocumentUnusableException(); - } - } - - private void validateCertificateLevel(CertificateResponse certificateResponse) { - String certificateLevel = certificateResponse.cert().certificateLevel(); - if (StringUtil.isEmpty(certificateLevel)) { - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); - } - if (!CertificateLevel.isSupported(certificateLevel)) { - logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); - throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); - } - CertificateLevel requestedLevel = this.certificateLevel == null ? CertificateLevel.QUALIFIED : this.certificateLevel; - if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(requestedLevel)) { - throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for constructing request to query certificate from Smart-ID API + */ +public class CertificateByDocumentNumberRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(CertificateByDocumentNumberRequestBuilder.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+={0,2}$"); + + private final SmartIdConnector connector; + + private String documentNumber; + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel = CertificateLevel.QUALIFIED; + + /** + * Constructs a new CertificateByDocumentNumberRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public CertificateByDocumentNumberRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the document number for the request. + * + * @param documentNumber the document number + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the relying party UUID for the request. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name for the request. + * + * @param relyingPartyName the relying party name + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level for the request. + * + * @param certificateLevel the certificate level + * @return this builder instance + */ + public CertificateByDocumentNumberRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Builds the request and retrieves the certificate by document number. + * + * @return CertificateByDocumentNumberResult containing the certificate level and parsed X509Certificate + * @throws SmartIdClientException if any required parameters are missing or invalid + * @throws UnprocessableSmartIdResponseException if the response is not valid + * @throws DocumentUnusableException if the document is unusable + */ + public CertificateByDocumentNumberResult getCertificateByDocumentNumber() { + validateRequestParameters(); + var request = new CertificateByDocumentNumberRequest(relyingPartyUUID, relyingPartyName, certificateLevel == null ? null : certificateLevel.name()); + CertificateResponse response = connector.getCertificateByDocumentNumber(documentNumber, request); + validateResponseParameters(response); + + return new CertificateByDocumentNumberResult( + CertificateLevel.valueOf(response.cert().certificateLevel()), + CertificateParser.parseX509Certificate(response.cert().value())); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(documentNumber)) { + throw new SmartIdClientException("Value for 'documentNumber' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdClientException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdClientException("Value for 'relyingPartyName' cannot be empty"); + } + } + + private void validateResponseParameters(CertificateResponse certificateResponse) { + if (certificateResponse == null) { + throw new UnprocessableSmartIdResponseException("Queried certificate response is not provided"); + } + validateState(certificateResponse); + + if (certificateResponse.cert() == null) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert' is missing"); + } + validateCertificateLevel(certificateResponse); + + if (StringUtil.isEmpty(certificateResponse.cert().value())) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' is missing"); + } + if (!BASE64_PATTERN.matcher(certificateResponse.cert().value()).matches()) { + logger.error("Certificate response field 'cert.value' has invalid value: {}", certificateResponse.cert().value()); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.value' does not have Base64-encoded value"); + } + } + + private static void validateState(CertificateResponse certificateResponse) { + String state = certificateResponse.state(); + if (StringUtil.isEmpty(state)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' is missing"); + } + if (!CertificateState.isSupported(state)) { + logger.error("Queried certificate response field 'state' has invalid value: {}", state); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'state' has unsupported value"); + } + if (CertificateState.valueOf(state) == CertificateState.DOCUMENT_UNUSABLE) { + throw new DocumentUnusableException(); + } + } + + private void validateCertificateLevel(CertificateResponse certificateResponse) { + String certificateLevel = certificateResponse.cert().certificateLevel(); + if (StringUtil.isEmpty(certificateLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' is missing"); + } + if (!CertificateLevel.isSupported(certificateLevel)) { + logger.error("Queried certificate response field 'cert.certificateLevel' has invalid value: {}", certificateLevel); + throw new UnprocessableSmartIdResponseException("Queried certificate response field 'cert.certificateLevel' has unsupported value"); + } + CertificateLevel requestedLevel = this.certificateLevel == null ? CertificateLevel.QUALIFIED : this.certificateLevel; + if (!CertificateLevel.valueOf(certificateLevel).isSameLevelOrHigher(requestedLevel)) { + throw new UnprocessableSmartIdResponseException("Queried certificate has lower level than requested"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java index 5e942eec..cafdf002 100644 --- a/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java +++ b/src/main/java/ee/sk/smartid/CertificateByDocumentNumberResult.java @@ -1,38 +1,38 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -/** - * Result of querying certificate by document number. - * - * @param certificateLevel the level of the certificate - * @param certificate the X.509 certificate - */ -public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +/** + * Result of querying certificate by document number. + * + * @param certificateLevel the level of the certificate + * @param certificate the X.509 certificate + */ +public record CertificateByDocumentNumberResult(CertificateLevel certificateLevel, X509Certificate certificate) { +} diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java index 15a1d1eb..d9dce29a 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponse.java @@ -1,140 +1,140 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -/** - * Represents the certificate choice response after a successful certificate choice sessions status response was received. - */ -public class CertificateChoiceResponse { - - private String endResult; - private X509Certificate certificate; - private CertificateLevel certificateLevel; - private String documentNumber; - private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name; Fix in SLIB-138 - private String deviceIpAddress; - - /** - * Gets the end result of the certificate choice session. - * - * @return the end result of the certificate choice session - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the certificate choice session. - * - * @param endResult the end result of the certificate choice session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the certificate chosen by the user during the certificate choice session. - * - * @return the certificate - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate chosen by the user during the certificate choice session. - * - * @param certificate the certificate from session status response - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the level of the certificate chosen by the user during the certificate choice session. - * - * @return the level of the certificate - */ - public CertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the level of the certificate chosen by the user during the certificate choice session. - * - * @param certificateLevel the level of the certificate from session status response - */ - public void setCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the document number of the user. - * - * @return the document number of the certificate - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number of the certificate chosen by the user during the certificate choice session. - * - * @param documentNumber the document number of the certificate from session status response - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * Gets the IP address of the device used in the certificate choice session. - * - * @return the IP address of the device - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in the certificate choice session. - * - * @param deviceIpAddress the IP address of the device from session status response - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +/** + * Represents the certificate choice response after a successful certificate choice sessions status response was received. + */ +public class CertificateChoiceResponse { + + private String endResult; + private X509Certificate certificate; + private CertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name; Fix in SLIB-138 + private String deviceIpAddress; + + /** + * Gets the end result of the certificate choice session. + * + * @return the end result of the certificate choice session + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the certificate choice session. + * + * @param endResult the end result of the certificate choice session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the certificate chosen by the user during the certificate choice session. + * + * @return the certificate + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate chosen by the user during the certificate choice session. + * + * @param certificate the certificate from session status response + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the level of the certificate chosen by the user during the certificate choice session. + * + * @return the level of the certificate + */ + public CertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the level of the certificate chosen by the user during the certificate choice session. + * + * @param certificateLevel the level of the certificate from session status response + */ + public void setCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the document number of the user. + * + * @return the document number of the certificate + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number of the certificate chosen by the user during the certificate choice session. + * + * @param documentNumber the document number of the certificate from session status response + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + /** + * Gets the IP address of the device used in the certificate choice session. + * + * @return the IP address of the device + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in the certificate choice session. + * + * @param deviceIpAddress the IP address of the device from session status response + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java index 01c03b4e..c4c76613 100644 --- a/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java +++ b/src/main/java/ee/sk/smartid/CertificateChoiceResponseValidator.java @@ -1,168 +1,168 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates and maps the received session status to certificate choice response - */ -public class CertificateChoiceResponseValidator { - - private static final Logger logger = LoggerFactory.getLogger(CertificateChoiceResponseValidator.class); - - private final CertificateValidator certificateValidator; - private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; - - /** - * Initializes the certificate choice response validator with a certificate validator - * - * @param certificateValidator certificate validator to validate the received certificate - */ - public CertificateChoiceResponseValidator(CertificateValidator certificateValidator) { - this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Initializes the certificate choice response validator with a certificate validator and signature certificate purpose validator factory - * - * @param certificateValidator certificate validator to validate the received certificate - * @param signatureCertificatePurposeValidatorFactory factory to create signature certificate purpose validators - */ - public CertificateChoiceResponseValidator(CertificateValidator certificateValidator, - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; - } - - /** - * Validates certificate choice session status response - *

- * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level - * - * @param sessionStatus session status received from Smart-ID server - * @return certificate choice response {@link CertificateChoiceResponse} - */ - public CertificateChoiceResponse validate(SessionStatus sessionStatus) { - return validate(sessionStatus, CertificateLevel.QUALIFIED); - } - - /** - * Validates session status to certificate choice response with the requested certificate level - * - * @param sessionStatus session status received from Smart-ID server - * @param requestedCertificateLevel requested certificate level - * @return certificate choice response {@link CertificateChoiceResponse} - * @throws SmartIdClientException when the parameters are not provided - * @throws UnprocessableSmartIdResponseException when any required field is missing from the response or has invalid value - * @throws CertificateLevelMismatchException when the returned certificate level is lower than the requested one - */ - public CertificateChoiceResponse validate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (requestedCertificateLevel == null) { - throw new SmartIdClientException("Parameter 'requestedCertificateLevel' is not provided"); - } - validateResult(sessionStatus.getResult()); - SessionCertificate sessionCertificate = sessionStatus.getCert(); - validateSessionStatusCertificate(sessionCertificate); - CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - X509Certificate certificate = getValidateX509Certificate(sessionCertificate, certificateLevel, requestedCertificateLevel); - return toCertificateChoiceResponse(sessionStatus, certificate, certificateLevel); - } - - private X509Certificate getValidateX509Certificate(SessionCertificate sessionCertificate, - CertificateLevel certificateLevel, - CertificateLevel requestedCertificateLevel) { - if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException("Certificate choice session status response certificate level is lower than requested"); - } - X509Certificate certificate = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); - certificateValidator.validate(certificate); - - SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); - purposeValidator.validate(certificate); - return certificate; - } - - private static void validateResult(SessionResult sessionResult) { - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result' is missing"); - } - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.endResult' is empty"); - } - if (!"OK".equalsIgnoreCase(endResult)) { - ErrorResultHandler.handle(sessionResult); - } - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.documentNumber' is empty"); - } - } - - private static void validateSessionStatusCertificate(SessionCertificate sessionCertificate) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert' is missing"); - } - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.value' has empty value"); - } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has empty value"); - } - if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Certificate choice session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has unsupported value"); - } - } - - private static CertificateChoiceResponse toCertificateChoiceResponse(SessionStatus sessionStatus, - X509Certificate certificate, - CertificateLevel certificateLevel) { - var certificateChoiceResponse = new CertificateChoiceResponse(); - certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); - certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); - certificateChoiceResponse.setCertificate(certificate); - certificateChoiceResponse.setCertificateLevel(certificateLevel); - certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - return certificateChoiceResponse; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates and maps the received session status to certificate choice response + */ +public class CertificateChoiceResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateChoiceResponseValidator.class); + + private final CertificateValidator certificateValidator; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; + + /** + * Initializes the certificate choice response validator with a certificate validator + * + * @param certificateValidator certificate validator to validate the received certificate + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Initializes the certificate choice response validator with a certificate validator and signature certificate purpose validator factory + * + * @param certificateValidator certificate validator to validate the received certificate + * @param signatureCertificatePurposeValidatorFactory factory to create signature certificate purpose validators + */ + public CertificateChoiceResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; + } + + /** + * Validates certificate choice session status response + *

+ * Uses {@link CertificateLevel#QUALIFIED} as the default for requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @return certificate choice response {@link CertificateChoiceResponse} + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus) { + return validate(sessionStatus, CertificateLevel.QUALIFIED); + } + + /** + * Validates session status to certificate choice response with the requested certificate level + * + * @param sessionStatus session status received from Smart-ID server + * @param requestedCertificateLevel requested certificate level + * @return certificate choice response {@link CertificateChoiceResponse} + * @throws SmartIdClientException when the parameters are not provided + * @throws UnprocessableSmartIdResponseException when any required field is missing from the response or has invalid value + * @throws CertificateLevelMismatchException when the returned certificate level is lower than the requested one + */ + public CertificateChoiceResponse validate(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (requestedCertificateLevel == null) { + throw new SmartIdClientException("Parameter 'requestedCertificateLevel' is not provided"); + } + validateResult(sessionStatus.getResult()); + SessionCertificate sessionCertificate = sessionStatus.getCert(); + validateSessionStatusCertificate(sessionCertificate); + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + X509Certificate certificate = getValidateX509Certificate(sessionCertificate, certificateLevel, requestedCertificateLevel); + return toCertificateChoiceResponse(sessionStatus, certificate, certificateLevel); + } + + private X509Certificate getValidateX509Certificate(SessionCertificate sessionCertificate, + CertificateLevel certificateLevel, + CertificateLevel requestedCertificateLevel) { + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException("Certificate choice session status response certificate level is lower than requested"); + } + X509Certificate certificate = CertificateParser.parseX509Certificate(sessionCertificate.getValue()); + certificateValidator.validate(certificate); + + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); + return certificate; + } + + private static void validateResult(SessionResult sessionResult) { + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result' is missing"); + } + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.endResult' is empty"); + } + if (!"OK".equalsIgnoreCase(endResult)) { + ErrorResultHandler.handle(sessionResult); + } + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'result.documentNumber' is empty"); + } + } + + private static void validateSessionStatusCertificate(SessionCertificate sessionCertificate) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert' is missing"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.value' has empty value"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has empty value"); + } + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Certificate choice session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Certificate choice session status field 'cert.certificateLevel' has unsupported value"); + } + } + + private static CertificateChoiceResponse toCertificateChoiceResponse(SessionStatus sessionStatus, + X509Certificate certificate, + CertificateLevel certificateLevel) { + var certificateChoiceResponse = new CertificateChoiceResponse(); + certificateChoiceResponse.setEndResult(sessionStatus.getResult().getEndResult()); + certificateChoiceResponse.setDocumentNumber(sessionStatus.getResult().getDocumentNumber()); + certificateChoiceResponse.setCertificate(certificate); + certificateChoiceResponse.setCertificateLevel(certificateLevel); + certificateChoiceResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + certificateChoiceResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + return certificateChoiceResponse; + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateLevel.java b/src/main/java/ee/sk/smartid/CertificateLevel.java index 21d3bfd4..d7373133 100644 --- a/src/main/java/ee/sk/smartid/CertificateLevel.java +++ b/src/main/java/ee/sk/smartid/CertificateLevel.java @@ -1,77 +1,77 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Representation of different signing certificate levels. - */ -public enum CertificateLevel { - - /** - * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. - */ - ADVANCED(1), - - /** - * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. - */ - QUALIFIED(2), - - /** - * Shortened alias for QUALIFIED level. - */ - QSCD(2); - - private final int level; - - CertificateLevel(int level) { - this.level = level; - } - - /** - * Check if current certificate level is same or higher than the given certificate level - * - * @param certificateLevel the level of the certificate - * @return true if the current level is same or higher than the given level, false otherwise - */ - public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { - return this == certificateLevel || this.level >= certificateLevel.level; - } - - /** - * Checks if the given certificate level value is supported - * - * @param certificateLevel the certificate level string to check - * @return true if the certificate level is supported, false otherwise - */ - public static boolean isSupported(String certificateLevel) { - return Arrays.stream(CertificateLevel.values()) - .anyMatch(level -> level.name().equals(certificateLevel)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Representation of different signing certificate levels. + */ +public enum CertificateLevel { + + /** + * Smart-ID basic certificate level. Use if you want to allow signing with non-qualified and qualified accounts. + */ + ADVANCED(1), + + /** + * The highest Smart-ID certificate level that is also QSCD-capable. Use only to allow signing with qualified accounts. + */ + QUALIFIED(2), + + /** + * Shortened alias for QUALIFIED level. + */ + QSCD(2); + + private final int level; + + CertificateLevel(int level) { + this.level = level; + } + + /** + * Check if current certificate level is same or higher than the given certificate level + * + * @param certificateLevel the level of the certificate + * @return true if the current level is same or higher than the given level, false otherwise + */ + public boolean isSameLevelOrHigher(CertificateLevel certificateLevel) { + return this == certificateLevel || this.level >= certificateLevel.level; + } + + /** + * Checks if the given certificate level value is supported + * + * @param certificateLevel the certificate level string to check + * @return true if the certificate level is supported, false otherwise + */ + public static boolean isSupported(String certificateLevel) { + return Arrays.stream(CertificateLevel.values()) + .anyMatch(level -> level.name().equals(certificateLevel)); + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateParser.java b/src/main/java/ee/sk/smartid/CertificateParser.java index 70f18ff9..bbb00467 100644 --- a/src/main/java/ee/sk/smartid/CertificateParser.java +++ b/src/main/java/ee/sk/smartid/CertificateParser.java @@ -1,73 +1,73 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for parsing X509 certificates from String values. - */ -public final class CertificateParser { - - private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); - - private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; - private static final String END_CERT = "-----END CERTIFICATE-----"; - - private CertificateParser() { - } - - /** - * Parses an X509 certificate from a String value. - * - * @param certificateValue the String value containing the certificate data - * @return the parsed X509Certificate - * @throws SmartIdClientException if the certificate cannot be parsed - */ - public static X509Certificate parseX509Certificate(String certificateValue) { - logger.debug("Parsing X509 certificate"); - String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( - StandardCharsets.UTF_8))); - } catch (CertificateException e) { - logger.error("Failed to parse X509 certificate from {}. Error {}", certificateString, e.getMessage()); - throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for parsing X509 certificates from String values. + */ +public final class CertificateParser { + + private static final Logger logger = LoggerFactory.getLogger(CertificateParser.class); + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private CertificateParser() { + } + + /** + * Parses an X509 certificate from a String value. + * + * @param certificateValue the String value containing the certificate data + * @return the parsed X509Certificate + * @throws SmartIdClientException if the certificate cannot be parsed + */ + public static X509Certificate parseX509Certificate(String certificateValue) { + logger.debug("Parsing X509 certificate"); + String certificateString = BEGIN_CERT + "\n" + certificateValue + "\n" + END_CERT; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateString.getBytes( + StandardCharsets.UTF_8))); + } catch (CertificateException e) { + logger.error("Failed to parse X509 certificate from {}. Error {}", certificateString, e.getMessage()); + throw new SmartIdClientException("Failed to parse X509 certificate from " + certificateString + ". Error " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/ee/sk/smartid/CertificateState.java b/src/main/java/ee/sk/smartid/CertificateState.java index 405b4ae9..6ade1924 100644 --- a/src/main/java/ee/sk/smartid/CertificateState.java +++ b/src/main/java/ee/sk/smartid/CertificateState.java @@ -1,57 +1,57 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Representation state of the queried certificate from the Smart-ID API. - */ -public enum CertificateState { - - /** - * Certificate is valid and can be used for signing. - */ - OK, - - /** - * There is an issue with the document. - */ - DOCUMENT_UNUSABLE; - - /** - * Checks if the given certificate state value is supported - * - * @param certificateState the certificate state string to check - * @return true if the certificate state is supported, false otherwise - */ - public static boolean isSupported(String certificateState) { - return Arrays.stream(CertificateState.values()) - .anyMatch(state -> state.name().equals(certificateState)); - } -} - +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Representation state of the queried certificate from the Smart-ID API. + */ +public enum CertificateState { + + /** + * Certificate is valid and can be used for signing. + */ + OK, + + /** + * There is an issue with the document. + */ + DOCUMENT_UNUSABLE; + + /** + * Checks if the given certificate state value is supported + * + * @param certificateState the certificate state string to check + * @return true if the certificate state is supported, false otherwise + */ + public static boolean isSupported(String certificateState) { + return Arrays.stream(CertificateState.values()) + .anyMatch(state -> state.name().equals(certificateState)); + } +} + diff --git a/src/main/java/ee/sk/smartid/CertificateValidator.java b/src/main/java/ee/sk/smartid/CertificateValidator.java index 1d74ddd0..0e499c12 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidator.java +++ b/src/main/java/ee/sk/smartid/CertificateValidator.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating X509 certificates used in Smart-ID authentication and signing. - *

- * Implementations of this interface should provide the logic to validate the certificate, - * ensuring it meets the necessary security and trust requirements. - */ -public interface CertificateValidator { - - /** - * Validates the given X509 certificate. - *

- * This method checks if the certificate is not expired and can be trusted - * - * @param certificate the X509Certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate is invalid - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating X509 certificates used in Smart-ID authentication and signing. + *

+ * Implementations of this interface should provide the logic to validate the certificate, + * ensuring it meets the necessary security and trust requirements. + */ +public interface CertificateValidator { + + /** + * Validates the given X509 certificate. + *

+ * This method checks if the certificate is not expired and can be trusted + * + * @param certificate the X509Certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is invalid + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java index 1bac0ed8..72dec7e1 100644 --- a/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/CertificateValidatorImpl.java @@ -1,106 +1,106 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertPathBuilder; -import java.security.cert.CertPathBuilderException; -import java.security.cert.CertStore; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.PKIXCertPathBuilderResult; -import java.security.cert.X509CertSelector; -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates the certificate's validity period and its trust chain. - */ -public class CertificateValidatorImpl implements CertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); - - private final TrustedCACertStore trustedCaCertStore; - - /** - * Constructs a certificate validator with the specified trusted certificate store. - * - * @param trustedCaCertStore the store containing trusted certificates. - */ - public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { - this.trustedCaCertStore = trustedCaCertStore; - } - - @Override - public void validate(X509Certificate certificate) { - validateCertificateIsCurrentlyValid(certificate); - validateCertificateChain(certificate); - } - - private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - logger.error("Certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); - throw new UnprocessableSmartIdResponseException("Certificate is invalid", ex); - } - } - - private void validateCertificateChain(X509Certificate certificate) { - try { - PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ - setCertificate(certificate); - }}); - CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); - params.addCertStore(intermediateStore); - params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); - CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); - PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); - - if (logger.isDebugEnabled()) { - X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); - X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); - X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", - CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), - CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); - } - } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { - throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderException; +import java.security.cert.CertStore; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXCertPathBuilderResult; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates the certificate's validity period and its trust chain. + */ +public class CertificateValidatorImpl implements CertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(CertificateValidatorImpl.class); + + private final TrustedCACertStore trustedCaCertStore; + + /** + * Constructs a certificate validator with the specified trusted certificate store. + * + * @param trustedCaCertStore the store containing trusted certificates. + */ + public CertificateValidatorImpl(TrustedCACertStore trustedCaCertStore) { + this.trustedCaCertStore = trustedCaCertStore; + } + + @Override + public void validate(X509Certificate certificate) { + validateCertificateIsCurrentlyValid(certificate); + validateCertificateChain(certificate); + } + + private static void validateCertificateIsCurrentlyValid(X509Certificate certificate) { + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Certificate is invalid", ex); + } + } + + private void validateCertificateChain(X509Certificate certificate) { + try { + PKIXBuilderParameters params = new PKIXBuilderParameters(trustedCaCertStore.getTrustAnchors(), new X509CertSelector() {{ + setCertificate(certificate); + }}); + CertStore intermediateStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(trustedCaCertStore.getTrustedCACertificates())); + params.addCertStore(intermediateStore); + params.setRevocationEnabled(trustedCaCertStore.isOcspEnabled()); + CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); + PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(params); + + if (logger.isDebugEnabled()) { + X509Certificate leaf = (X509Certificate) result.getCertPath().getCertificates().get(0); + X509Certificate intermediate = (X509Certificate) result.getCertPath().getCertificates().get(1); + X509Certificate trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Leaf: {}, Intermediate: {}, Trust anchor: {}", + CertificateAttributeUtil.getAttributeValue(leaf.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(intermediate.getSubjectX500Principal().getName(), BCStyle.CN), + CertificateAttributeUtil.getAttributeValue(trustedCert.getSubjectX500Principal().getName(), BCStyle.CN)); + } + } catch (InvalidAlgorithmParameterException | CertPathBuilderException | NoSuchAlgorithmException ex) { + throw new UnprocessableSmartIdResponseException("Certificate chain validation failed", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java index f29469e1..10ad8eaa 100644 --- a/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCACertStore.java @@ -1,86 +1,86 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Implementation of the TrustedCAStore that manages a collection of trusted CA certificates. - */ -public class DefaultTrustedCACertStore implements TrustedCACertStore { - - private final Set trustAnchors = new HashSet<>(); - private final List trustedCACertificates = new ArrayList<>(); - private final boolean ocspEnabled; - - /** - * Initializes the trusted CA certificates from an array of X509 certificates. - * - * @param trustAnchors a set of TrustAnchor objects representing the trust anchors - * @param trustedCaCertificates a list of X509Certificate objects representing the trusted CA certificates - * @param ocspEnabled flag to disable or active OCSP validations - * - * @throws SmartIdClientException if the provided array is null or empty - */ - - public DefaultTrustedCACertStore(Set trustAnchors, List trustedCaCertificates, boolean ocspEnabled) { - this.trustAnchors.addAll(trustAnchors); - trustedCACertificates.addAll(trustedCaCertificates); - this.ocspEnabled = ocspEnabled; - } - - @Override - public List getTrustedCACertificates() { - return List.copyOf(trustedCACertificates); - } - - @Override - public Set getTrustAnchors() { - return Set.copyOf(trustAnchors); - } - - @Override - public boolean isOcspEnabled() { - return ocspEnabled; - } - - interface Builder { - /** - * Builds a new TrustedCAStoreImpl instance with the specified configuration. - * - * @return a new TrustedCAStoreImpl instance - */ - TrustedCACertStore build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Implementation of the TrustedCAStore that manages a collection of trusted CA certificates. + */ +public class DefaultTrustedCACertStore implements TrustedCACertStore { + + private final Set trustAnchors = new HashSet<>(); + private final List trustedCACertificates = new ArrayList<>(); + private final boolean ocspEnabled; + + /** + * Initializes the trusted CA certificates from an array of X509 certificates. + * + * @param trustAnchors a set of TrustAnchor objects representing the trust anchors + * @param trustedCaCertificates a list of X509Certificate objects representing the trusted CA certificates + * @param ocspEnabled flag to disable or active OCSP validations + * + * @throws SmartIdClientException if the provided array is null or empty + */ + + public DefaultTrustedCACertStore(Set trustAnchors, List trustedCaCertificates, boolean ocspEnabled) { + this.trustAnchors.addAll(trustAnchors); + trustedCACertificates.addAll(trustedCaCertificates); + this.ocspEnabled = ocspEnabled; + } + + @Override + public List getTrustedCACertificates() { + return List.copyOf(trustedCACertificates); + } + + @Override + public Set getTrustAnchors() { + return Set.copyOf(trustAnchors); + } + + @Override + public boolean isOcspEnabled() { + return ocspEnabled; + } + + interface Builder { + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instance + */ + TrustedCACertStore build(); + } +} diff --git a/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java index a00118d9..4244bd54 100644 --- a/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java +++ b/src/main/java/ee/sk/smartid/DefaultTrustedCAStoreBuilder.java @@ -1,158 +1,158 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.GeneralSecurityException; -import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertStore; -import java.security.cert.CertificateFactory; -import java.security.cert.CollectionCertStoreParameters; -import java.security.cert.PKIXCertPathValidatorResult; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Builder for creating a DefaultTrustedCACertStore instance. - * This builder allows setting trust anchors, trusted CA certificates, and OCSP validation settings. - */ -public class DefaultTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { - - private static final Logger logger = LoggerFactory.getLogger(DefaultTrustedCAStoreBuilder.class); - - private Set trustAnchors; - private List intermediateCACertificates; - private boolean ocspEnabled = true; - private X509Certificate ocspValidationCert; - - /** - * Sets the trust anchors for the TrustedCAStore. - * - * @param trustAnchors a set of TrustAnchor objects to be used as trust anchors - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withTrustAnchors(Set trustAnchors) { - this.trustAnchors = trustAnchors; - return this; - } - - /** - * Sets the trusted CA certificates for the TrustedCAStore. - * - * @param intermediateCACertificates a list of X509Certificate objects to be used as trusted CA certificates - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withIntermediateCACertificate(List intermediateCACertificates) { - this.intermediateCACertificates = List.copyOf(intermediateCACertificates); - return this; - } - - /** - * Sets whether OCSP (Online Certificate Status Protocol) validation is enabled. - * - * @param enabled true to enable OCSP validation, false to disable it - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { - this.ocspEnabled = enabled; - return this; - } - - /** - * Sets the certificate used for OCSP validation. - * - * @param ocspValidationCert the X509Certificate to be used for OCSP validation - * @return this Builder instance - */ - public DefaultTrustedCAStoreBuilder withOCSPValidationCert(X509Certificate ocspValidationCert) { - this.ocspValidationCert = ocspValidationCert; - return this; - } - - @Override - public DefaultTrustedCACertStore build() { - if (!ocspEnabled) { - logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); - } else { - throw new UnsupportedOperationException("Does not work yet, will be implemented later"); - } - validateTrustAnchors(); - validateIntermediateCaCertificates(); - return new DefaultTrustedCACertStore(Set.copyOf(trustAnchors), List.copyOf(intermediateCACertificates), ocspEnabled); - } - - private void validateTrustAnchors() { - for (TrustAnchor trustAnchor : trustAnchors) { - try { - trustAnchor.getTrustedCert().verify(trustAnchor.getTrustedCert().getPublicKey()); - } catch (GeneralSecurityException e) { - throw new SmartIdClientException("", e); - } - } - } - - private void validateIntermediateCaCertificates() { - for (X509Certificate cert : intermediateCACertificates) { - validateIntermediateCACertificate(cert); - } - } - - private void validateIntermediateCACertificate(X509Certificate x509Certificates) { - try { - var cf = CertificateFactory.getInstance("X.509"); - CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); - var pkixParameters = new PKIXParameters(trustAnchors); - pkixParameters.setRevocationEnabled(ocspEnabled); - if (ocspEnabled) { - var certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(ocspValidationCert))); - pkixParameters.setCertStores(List.of(certStore)); - } - var certPathValidator = CertPathValidator.getInstance("PKIX"); - var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); - var trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); - } catch (GeneralSecurityException ex) { - logger.error("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); - throw new SmartIdClientException("Validating intermediate CA failed", ex); - } - } - - private String getCNValue(X509Certificate certificate) { - String subjectDN = certificate.getSubjectX500Principal().getName(); - return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertStore; +import java.security.cert.CertificateFactory; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Builder for creating a DefaultTrustedCACertStore instance. + * This builder allows setting trust anchors, trusted CA certificates, and OCSP validation settings. + */ +public class DefaultTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(DefaultTrustedCAStoreBuilder.class); + + private Set trustAnchors; + private List intermediateCACertificates; + private boolean ocspEnabled = true; + private X509Certificate ocspValidationCert; + + /** + * Sets the trust anchors for the TrustedCAStore. + * + * @param trustAnchors a set of TrustAnchor objects to be used as trust anchors + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withTrustAnchors(Set trustAnchors) { + this.trustAnchors = trustAnchors; + return this; + } + + /** + * Sets the trusted CA certificates for the TrustedCAStore. + * + * @param intermediateCACertificates a list of X509Certificate objects to be used as trusted CA certificates + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withIntermediateCACertificate(List intermediateCACertificates) { + this.intermediateCACertificates = List.copyOf(intermediateCACertificates); + return this; + } + + /** + * Sets whether OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @param enabled true to enable OCSP validation, false to disable it + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Sets the certificate used for OCSP validation. + * + * @param ocspValidationCert the X509Certificate to be used for OCSP validation + * @return this Builder instance + */ + public DefaultTrustedCAStoreBuilder withOCSPValidationCert(X509Certificate ocspValidationCert) { + this.ocspValidationCert = ocspValidationCert; + return this; + } + + @Override + public DefaultTrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("Does not work yet, will be implemented later"); + } + validateTrustAnchors(); + validateIntermediateCaCertificates(); + return new DefaultTrustedCACertStore(Set.copyOf(trustAnchors), List.copyOf(intermediateCACertificates), ocspEnabled); + } + + private void validateTrustAnchors() { + for (TrustAnchor trustAnchor : trustAnchors) { + try { + trustAnchor.getTrustedCert().verify(trustAnchor.getTrustedCert().getPublicKey()); + } catch (GeneralSecurityException e) { + throw new SmartIdClientException("", e); + } + } + } + + private void validateIntermediateCaCertificates() { + for (X509Certificate cert : intermediateCACertificates) { + validateIntermediateCACertificate(cert); + } + } + + private void validateIntermediateCACertificate(X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + if (ocspEnabled) { + var certStore = CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(ocspValidationCert))); + pkixParameters.setCertStores(List.of(certStore)); + } + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.error("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java index cf78d76b..55496621 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidator.java @@ -1,216 +1,216 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates authentication response and converts it to {@link AuthenticationIdentity} - */ -public class DeviceLinkAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; - - /** - * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} - * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value - * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validator based on certificate level - */ - public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; - } - - /** - * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} using {@link CertificateValidator} - * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @return a new instance of {@link DeviceLinkAuthenticationResponseValidator} - */ - public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { - return new DeviceLinkAuthenticationResponseValidator(certificateValidator, - new AuthenticationResponseMapperImpl(), - new SignatureValueValidatorImpl(), - new AuthenticationCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates the authentication response contained in the session status using the provided authentication session request. - * - * @param sessionStatus the session status containing the authentication response to be validated - * @param authenticationSessionRequest the authentication session request used to initiate the authentication session - * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. - * Required only for same device flows. - * @param schemaName Schema name (RP name) used in the device link - * @return Authentication identity containing details about the authenticated user - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String userChallengeVerifier, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, null); - } - - /** - * Validates the authentication response contained in the session status using the provided authentication session request. - * - * @param sessionStatus the session status containing the authentication response to be validated - * @param authenticationSessionRequest the authentication session request used to initiate the authentication session - * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. - * Required only for same device flows. - * @param schemaName Schema name (RP name) used in the device link - * @param brokeredRpName the brokered RP name, used in the device link - * @return Authentication identity containing details about the authenticated user - * @throws UnprocessableSmartIdResponseException if the authentication response is invalid - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String userChallengeVerifier, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateUserChallenge(userChallengeVerifier, authenticationResponse); - validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - private void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { - return authenticationSessionRequest == null - ? AuthenticationCertificateLevel.QUALIFIED - : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - String[] payload = { - schemaName, - SignatureProtocol.ACSP_V2.name(), - authenticationResponse.getServerRandom(), - authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), - StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - toBase64(authenticationSessionRequest.relyingPartyName()), - StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), - authenticationResponse.getInteractionTypeUsed(), - authenticationResponse.getFlowType() == FlowType.QR ? "" : authenticationSessionRequest.initialCallbackUrl(), - authenticationResponse.getFlowType().getDescription() - }; - return String - .join("|", payload) - .getBytes(StandardCharsets.UTF_8); - } - - private static void validateUserChallenge(String userChallengeVerifier, AuthenticationResponse authenticationResponse) { - if (authenticationResponse.getFlowType() != FlowType.WEB2APP - && authenticationResponse.getFlowType() != FlowType.APP2APP) { - return; - } - if (StringUtil.isEmpty(userChallengeVerifier)) { - throw new SmartIdClientException("Parameter 'userChallengeVerifier' must be provided for 'flowType' - " + authenticationResponse.getFlowType()); - } - String userChallenge = authenticationResponse.getUserChallenge(); - String urlUserChallenge = toDigest(userChallengeVerifier); - if (!userChallenge.equals(urlUserChallenge)) { - throw new UnprocessableSmartIdResponseException("Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"); - } - } - - private static String toDigest(String userChallengeVerifier) { - byte[] userChallengeVerifierDigest = DigestCalculator.calculateDigest(userChallengeVerifier.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - return Base64.getUrlEncoder().withoutPadding().encodeToString(userChallengeVerifierDigest); - } - - private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private static String toBase64(String input) { - return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates authentication response and converts it to {@link AuthenticationIdentity} + */ +public class DeviceLinkAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + + /** + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validator based on certificate level + */ + public DeviceLinkAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + } + + /** + * Creates an instance of {@link DeviceLinkAuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @return a new instance of {@link DeviceLinkAuthenticationResponseValidator} + */ + public static DeviceLinkAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new DeviceLinkAuthenticationResponseValidator(certificateValidator, + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @return Authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, userChallengeVerifier, schemaName, null); + } + + /** + * Validates the authentication response contained in the session status using the provided authentication session request. + * + * @param sessionStatus the session status containing the authentication response to be validated + * @param authenticationSessionRequest the authentication session request used to initiate the authentication session + * @param userChallengeVerifier the user challenge verifier from callback URL to validate against the user challenge in the authentication response. + * Required only for same device flows. + * @param schemaName Schema name (RP name) used in the device link + * @param brokeredRpName the brokered RP name, used in the device link + * @return Authentication identity containing details about the authenticated user + * @throws UnprocessableSmartIdResponseException if the authentication response is invalid + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String userChallengeVerifier, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateUserChallenge(userChallengeVerifier, authenticationResponse); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(DeviceLinkAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), + authenticationResponse.getInteractionTypeUsed(), + authenticationResponse.getFlowType() == FlowType.QR ? "" : authenticationSessionRequest.initialCallbackUrl(), + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); + } + + private static void validateUserChallenge(String userChallengeVerifier, AuthenticationResponse authenticationResponse) { + if (authenticationResponse.getFlowType() != FlowType.WEB2APP + && authenticationResponse.getFlowType() != FlowType.APP2APP) { + return; + } + if (StringUtil.isEmpty(userChallengeVerifier)) { + throw new SmartIdClientException("Parameter 'userChallengeVerifier' must be provided for 'flowType' - " + authenticationResponse.getFlowType()); + } + String userChallenge = authenticationResponse.getUserChallenge(); + String urlUserChallenge = toDigest(userChallengeVerifier); + if (!userChallenge.equals(urlUserChallenge)) { + throw new UnprocessableSmartIdResponseException("Device link authentication 'signature.userChallenge' does not validate with 'userChallengeVerifier'"); + } + } + + private static String toDigest(String userChallengeVerifier) { + byte[] userChallengeVerifierDigest = DigestCalculator.calculateDigest(userChallengeVerifier.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(userChallengeVerifierDigest); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java index e46c6878..a7451f7e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilder.java @@ -1,361 +1,361 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a device-link authentication session - */ -public class DeviceLinkAuthenticationSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; - private String rpChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; - private List interactions; - private Boolean shareMdClientIpAddress; - private Set capabilities; - private SemanticsIdentifier semanticsIdentifier; - private String documentNumber; - private String initialCallbackUrl; - - private DeviceLinkAuthenticationSessionRequest authenticationSessionRequest; - - /** - * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public DeviceLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartUUID the relying party UUID - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { - this.relyingPartyUUID = relyingPartUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - *

- * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the RP challenge. - *

- * RP challenge is a randomly generated string that must be Base64 encoded and - * should be regenerated for every new authentication session request. - *

- * You can use {@link ee.sk.smartid.RpChallengeGenerator} to generate a suitable RP challenge. - * - * @param rpChallenge RP challenge in Base64 encoded format - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { - this.rpChallenge = rpChallenge; - return this; - } - - /** - * Sets the signature algorithm - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the hash algorithm to be used for signature creation. - * By default, SHA3-512 is used. - * - * @param hashAlgorithm the hash algorithm to use - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return this; - } - - /** - * Sets the allowed interactions order - * - * @param interactions the allowed interactions order - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the authentication session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the document number - *

- * Setting this value will make the authentication session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the initial callback URL. - *

- * This URL is used to redirect the user after the authentication session is started. - *

- * The callback URL should be set when using same device flows (like Web2App or App2App). - * - * @param initialCallbackUrl the initial callback URL - * @return this builder - */ - public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Sends the authentication request and get the init session response - *

- * There are 3 supported ways to start authentication session: - *

    - *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
  • with document number by using {@link #withDocumentNumber(String)}
  • - *
  • anonymously if semantics identifier and document number are not provided
  • - *
- * - * @return init session response - * @throws SmartIdRequestSetupException if the provided values for the request are invalid - * @throws UnprocessableSmartIdResponseException if the response is missing required fields - */ - public DeviceLinkSessionResponse initAuthenticationSession() { - validateRequestParameters(); - DeviceLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); - validateResponseParameters(deviceLinkAuthenticationSessionResponse); - this.authenticationSessionRequest = authenticationRequest; - return deviceLinkAuthenticationSessionResponse; - } - - /** - * Returns the authentication session request created during the initialization - * - * @return the authentication session request - * @throws SmartIdClientException when session is not yet initialized and method is called - */ - public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Device link authentication session has not been initialized yet"); - } - return authenticationSessionRequest; - } - - private DeviceLinkSessionResponse initAuthenticationSession(DeviceLinkAuthenticationSessionRequest authenticationRequest) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (semanticsIdentifier != null) { - return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initDeviceLinkAuthentication(authenticationRequest, documentNumber); - } else { - return connector.initAnonymousDeviceLinkAuthentication(authenticationRequest); - } - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - validateSignatureParameters(); - validateInteractions(); - validateInitialCallbackUrl(); - } - - private void validateSignatureParameters() { - if (StringUtil.isEmpty(rpChallenge)) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); - } - try { - Base64.getDecoder().decode(rpChallenge); - } catch (IllegalArgumentException e) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); - } - if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private DeviceLinkAuthenticationSessionRequest createAuthenticationRequest() { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); - - return new DeviceLinkAuthenticationSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2, - signatureProtocolParameters, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - capabilities, - initialCallbackUrl - ); - } - - private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); - } - if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null - || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a device-link authentication session + */ +public class DeviceLinkAuthenticationSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel = AuthenticationCertificateLevel.QUALIFIED; + private String rpChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + private String initialCallbackUrl; + + private DeviceLinkAuthenticationSessionRequest authenticationSessionRequest; + + /** + * Constructs a new DeviceLinkAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DeviceLinkAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + *

+ * Defaults to {@link AuthenticationCertificateLevel#QUALIFIED} + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the RP challenge. + *

+ * RP challenge is a randomly generated string that must be Base64 encoded and + * should be regenerated for every new authentication session request. + *

+ * You can use {@link ee.sk.smartid.RpChallengeGenerator} to generate a suitable RP challenge. + * + * @param rpChallenge RP challenge in Base64 encoded format + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the hash algorithm to be used for signature creation. + * By default, SHA3-512 is used. + * + * @param hashAlgorithm the hash algorithm to use + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the allowed interactions order + * + * @param interactions the allowed interactions order + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

+ * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the initial callback URL. + *

+ * This URL is used to redirect the user after the authentication session is started. + *

+ * The callback URL should be set when using same device flows (like Web2App or App2App). + * + * @param initialCallbackUrl the initial callback URL + * @return this builder + */ + public DeviceLinkAuthenticationSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

+ * There are 3 supported ways to start authentication session: + *

    + *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
  • with document number by using {@link #withDocumentNumber(String)}
  • + *
  • anonymously if semantics identifier and document number are not provided
  • + *
+ * + * @return init session response + * @throws SmartIdRequestSetupException if the provided values for the request are invalid + * @throws UnprocessableSmartIdResponseException if the response is missing required fields + */ + public DeviceLinkSessionResponse initAuthenticationSession() { + validateRequestParameters(); + DeviceLinkAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(deviceLinkAuthenticationSessionResponse); + this.authenticationSessionRequest = authenticationRequest; + return deviceLinkAuthenticationSessionResponse; + } + + /** + * Returns the authentication session request created during the initialization + * + * @return the authentication session request + * @throws SmartIdClientException when session is not yet initialized and method is called + */ + public DeviceLinkAuthenticationSessionRequest getAuthenticationSessionRequest() { + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Device link authentication session has not been initialized yet"); + } + return authenticationSessionRequest; + } + + private DeviceLinkSessionResponse initAuthenticationSession(DeviceLinkAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (semanticsIdentifier != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initDeviceLinkAuthentication(authenticationRequest, documentNumber); + } else { + return connector.initAnonymousDeviceLinkAuthentication(authenticationRequest); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + validateSignatureParameters(); + validateInteractions(); + validateInitialCallbackUrl(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(rpChallenge)) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); + } + try { + Base64.getDecoder().decode(rpChallenge); + } catch (IllegalArgumentException e) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); + } + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private DeviceLinkAuthenticationSessionRequest createAuthenticationRequest() { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(this.hashAlgorithm.getAlgorithmName())); + + return new DeviceLinkAuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + initialCallbackUrl + ); + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkAuthenticationSessionResponse) { + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionID' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionToken' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkAuthenticationSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'sessionSecret' is missing or empty"); + } + if (deviceLinkAuthenticationSessionResponse.deviceLinkBase() == null + || deviceLinkAuthenticationSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java index 915551c7..a9a3f26b 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkBuilder.java @@ -1,361 +1,361 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.StringUtil; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Builder for creating Smart-ID device-link URI. - */ -public class DeviceLinkBuilder { - - private static final String ALLOWED_VERSION = "1.0"; - - private String schemeName = "smart-id"; - private String deviceLinkBase; - private String version = ALLOWED_VERSION; - private DeviceLinkType deviceLinkType; - private SessionType sessionType; - private String sessionToken; - private Long elapsedSeconds; - private String lang; - - private String digest; - private String relyingPartyNameBase64; - private String brokeredRpNameBase64; - private String interactions; - private String initialCallbackUrl; - - /** - * Sets the scheme name for the device link. - *

- * Default is `smart-id`. - * - * @param schemeName the scheme name to be used in the device link - * @return this builder - */ - public DeviceLinkBuilder withSchemeName(String schemeName) { - this.schemeName = schemeName; - return this; - } - - /** - * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. - *

- * This is a required parameter and must be taken from the `deviceLinkBase` value received in the session-init response. - * - * @param deviceLinkBase the URL that will direct to SMART-ID application - * @return this builder - */ - public DeviceLinkBuilder withDeviceLinkBase(String deviceLinkBase) { - this.deviceLinkBase = deviceLinkBase; - return this; - } - - /** - * Sets the version of the device link. - *

- * Only value 1.0 is allowed - * - * @param version the version of - * @return this builder - */ - public DeviceLinkBuilder withVersion(String version) { - this.version = version; - return this; - } - - /** - * Sets the type of the device link. Use {@link DeviceLinkType} to set the type. - * - * @param deviceLinkType the type of the device link the builder is creating - * @return this builder - */ - public DeviceLinkBuilder withDeviceLinkType(DeviceLinkType deviceLinkType) { - this.deviceLinkType = deviceLinkType; - return this; - } - - /** - * Sets the type of the session. Use {@link SessionType} to set the type. - * - * @param sessionType the type of the session the device link is created for - * @return this builder - */ - public DeviceLinkBuilder withSessionType(SessionType sessionType) { - this.sessionType = sessionType; - return this; - } - - /** - * Sets the session token that was received from the Smart-ID server. - * - * @param sessionToken the session token that was received from the Smart-ID server - * @return this builder - */ - public DeviceLinkBuilder withSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - return this; - } - - /** - * Sets the time passed since the session response was received. - * Only valid for QR_CODE device link type. - * - * @param elapsedSeconds the time passed since the session response was received in seconds - * @return this builder - */ - public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { - this.elapsedSeconds = elapsedSeconds; - return this; - } - - /** - * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. - *

- * Default value is "eng". - * The value must match the language shown to the user in the UI. - * Also used for the fallback web page if the Smart-ID app is not installed. - * - * @param lang the language of the user - * @return this builder - */ - public DeviceLinkBuilder withLang(String lang) { - this.lang = lang; - return this; - } - - /** - * Sets the digest or rpChallenge used in the session. - * Required when signatureProtocol is defined. - * - * @param digest the digest or rpChallenge value - * @return this builder - */ - public DeviceLinkBuilder withDigest(String digest) { - this.digest = digest; - return this; - } - - /** - * Sets the relying party name which will be Base64-encoded using UTF-8. - * - * @param relyingPartyName relying party name as plain UTF-8 string - * @return this builder - */ - public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyNameBase64 = Base64.getEncoder().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); - return this; - } - - /** - * Sets the brokered relying party name which will be Base64-encoded using UTF-8. - * Leave empty if not acting as a broker. - * - * @param brokeredRpName brokered RP name as plain UTF-8 string - * @return this builder - */ - public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { - this.brokeredRpNameBase64 = Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); - return this; - } - - /** - * Sets the interactions used during session initiation as Base64 string. - * - * @param interactions interactions string in Base64 - * @return this builder - */ - public DeviceLinkBuilder withInteractions(String interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets the callback URL used in session initiation. - * Required only for same device flows (Web2App and App2App). - * Must be left empty for QR-code flow. - * - * @param initialCallbackUrl initial callback URL - * @return this builder - */ - public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Builds a Smart-ID device-link URI without authentication code. - *

- * The resulting URI is used in Web2App, App2App or QR-code flows, - * and must be combined with an authCode to form a valid device-link. - * - * @return unprotected device link URI - */ - public URI createUnprotectedUri() { - validateInputParameters(); - UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); - addElapsedSecondsIfQrCode(uriBuilder); - uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) - .queryParam("version", version).queryParam("lang", lang); - return uriBuilder.build(); - } - - /** - * Builds the final Smart-ID device link URI by combining unprotected link and authCode. - * - * @param sessionSecret session secret received from session initialization response. - * @return full device link URI with authCode parameter - */ - public URI buildDeviceLink(String sessionSecret) { - URI unprotectedUri = createUnprotectedUri(); - String authCode = generateAuthCode(unprotectedUri.toString(), sessionSecret); - return UriBuilder.fromUri(unprotectedUri) - .queryParam("authCode", authCode) - .build(); - } - - private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { - if (elapsedSeconds != null) { - if (deviceLinkType != DeviceLinkType.QR_CODE) { - throw new SmartIdClientException("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE"); - } - uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); - } - } - - private String generateAuthCode(String unprotectedLink, String sessionSecret) { - if (StringUtil.isEmpty(sessionSecret)) { - throw new SmartIdClientException("Parameter 'sessionSecret' cannot be empty"); - } - validateAuthCodeParams(); - return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); - } - - private String buildPayload(String unprotectedLink) { - return String.join("|", - schemeName, - getSignatureProtocolForSession(), - StringUtil.orEmpty(digest), - relyingPartyNameBase64, - StringUtil.orEmpty(brokeredRpNameBase64), - StringUtil.orEmpty(interactions), - StringUtil.orEmpty(initialCallbackUrl), - unprotectedLink - ); - } - - private String getSignatureProtocolForSession() { - return switch (sessionType) { - case AUTHENTICATION -> SignatureProtocol.ACSP_V2.name(); - case SIGNATURE -> SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); - case CERTIFICATE_CHOICE -> ""; - }; - } - - private String calculateAuthCode(String data, String base64Key) { - try { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); - byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); - return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); - } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException ex) { - throw new SmartIdClientException("Failed to calculate authCode", ex); - } - } - - private void validateInputParameters() { - if (StringUtil.isEmpty(deviceLinkBase)) { - throw new SmartIdClientException("Parameter 'deviceLinkBase' cannot be empty"); - } - if (StringUtil.isEmpty(version)) { - throw new SmartIdClientException("Parameter 'version' cannot be empty"); - } - if (!ALLOWED_VERSION.equals(version)) { - throw new SmartIdClientException("Only version 1.0 is allowed"); - } - if (deviceLinkType == null) { - throw new SmartIdClientException("Parameter 'deviceLinkType' must be set"); - } - if (sessionType == null) { - throw new SmartIdClientException("Parameter 'sessionType' must be set"); - } - if (StringUtil.isEmpty(sessionToken)) { - throw new SmartIdClientException("Parameter 'sessionToken' cannot be empty"); - } - if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { - throw new SmartIdClientException("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE"); - } - if (StringUtil.isEmpty(lang)) { - throw new SmartIdClientException("Parameter 'lang' must be set"); - } - } - - private void validateAuthCodeParams() { - if (StringUtil.isEmpty(schemeName)) { - throw new SmartIdClientException("Parameter 'schemeName' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyNameBase64)) { - throw new SmartIdClientException("Parameter 'relyingPartyName' cannot be empty"); - } - - boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); - if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { - throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE"); - } - if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { - throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP"); - } - if (sessionType != SessionType.CERTIFICATE_CHOICE) { - if (StringUtil.isEmpty(digest)) { - throw new SmartIdClientException("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); - } - if (StringUtil.isEmpty(interactions)) { - throw new SmartIdClientException("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); - } - } - if (sessionType == SessionType.CERTIFICATE_CHOICE) { - if (StringUtil.isNotEmpty(digest)) { - throw new SmartIdClientException("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); - } - if (StringUtil.isNotEmpty(interactions)) { - throw new SmartIdClientException("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); - } - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.StringUtil; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Builder for creating Smart-ID device-link URI. + */ +public class DeviceLinkBuilder { + + private static final String ALLOWED_VERSION = "1.0"; + + private String schemeName = "smart-id"; + private String deviceLinkBase; + private String version = ALLOWED_VERSION; + private DeviceLinkType deviceLinkType; + private SessionType sessionType; + private String sessionToken; + private Long elapsedSeconds; + private String lang; + + private String digest; + private String relyingPartyNameBase64; + private String brokeredRpNameBase64; + private String interactions; + private String initialCallbackUrl; + + /** + * Sets the scheme name for the device link. + *

+ * Default is `smart-id`. + * + * @param schemeName the scheme name to be used in the device link + * @return this builder + */ + public DeviceLinkBuilder withSchemeName(String schemeName) { + this.schemeName = schemeName; + return this; + } + + /** + * Sets the base URI to which all query parameters will be appended to form the full Smart-ID device link. + *

+ * This is a required parameter and must be taken from the `deviceLinkBase` value received in the session-init response. + * + * @param deviceLinkBase the URL that will direct to SMART-ID application + * @return this builder + */ + public DeviceLinkBuilder withDeviceLinkBase(String deviceLinkBase) { + this.deviceLinkBase = deviceLinkBase; + return this; + } + + /** + * Sets the version of the device link. + *

+ * Only value 1.0 is allowed + * + * @param version the version of + * @return this builder + */ + public DeviceLinkBuilder withVersion(String version) { + this.version = version; + return this; + } + + /** + * Sets the type of the device link. Use {@link DeviceLinkType} to set the type. + * + * @param deviceLinkType the type of the device link the builder is creating + * @return this builder + */ + public DeviceLinkBuilder withDeviceLinkType(DeviceLinkType deviceLinkType) { + this.deviceLinkType = deviceLinkType; + return this; + } + + /** + * Sets the type of the session. Use {@link SessionType} to set the type. + * + * @param sessionType the type of the session the device link is created for + * @return this builder + */ + public DeviceLinkBuilder withSessionType(SessionType sessionType) { + this.sessionType = sessionType; + return this; + } + + /** + * Sets the session token that was received from the Smart-ID server. + * + * @param sessionToken the session token that was received from the Smart-ID server + * @return this builder + */ + public DeviceLinkBuilder withSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + /** + * Sets the time passed since the session response was received. + * Only valid for QR_CODE device link type. + * + * @param elapsedSeconds the time passed since the session response was received in seconds + * @return this builder + */ + public DeviceLinkBuilder withElapsedSeconds(Long elapsedSeconds) { + this.elapsedSeconds = elapsedSeconds; + return this; + } + + /** + * Sets the language of the user. The language must be given as a 3-letter ISO 639-2 language code. + *

+ * Default value is "eng". + * The value must match the language shown to the user in the UI. + * Also used for the fallback web page if the Smart-ID app is not installed. + * + * @param lang the language of the user + * @return this builder + */ + public DeviceLinkBuilder withLang(String lang) { + this.lang = lang; + return this; + } + + /** + * Sets the digest or rpChallenge used in the session. + * Required when signatureProtocol is defined. + * + * @param digest the digest or rpChallenge value + * @return this builder + */ + public DeviceLinkBuilder withDigest(String digest) { + this.digest = digest; + return this; + } + + /** + * Sets the relying party name which will be Base64-encoded using UTF-8. + * + * @param relyingPartyName relying party name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyNameBase64 = Base64.getEncoder().encodeToString(relyingPartyName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the brokered relying party name which will be Base64-encoded using UTF-8. + * Leave empty if not acting as a broker. + * + * @param brokeredRpName brokered RP name as plain UTF-8 string + * @return this builder + */ + public DeviceLinkBuilder withBrokeredRpName(String brokeredRpName) { + this.brokeredRpNameBase64 = Base64.getEncoder().encodeToString(brokeredRpName.getBytes(StandardCharsets.UTF_8)); + return this; + } + + /** + * Sets the interactions used during session initiation as Base64 string. + * + * @param interactions interactions string in Base64 + * @return this builder + */ + public DeviceLinkBuilder withInteractions(String interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets the callback URL used in session initiation. + * Required only for same device flows (Web2App and App2App). + * Must be left empty for QR-code flow. + * + * @param initialCallbackUrl initial callback URL + * @return this builder + */ + public DeviceLinkBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Builds a Smart-ID device-link URI without authentication code. + *

+ * The resulting URI is used in Web2App, App2App or QR-code flows, + * and must be combined with an authCode to form a valid device-link. + * + * @return unprotected device link URI + */ + public URI createUnprotectedUri() { + validateInputParameters(); + UriBuilder uriBuilder = UriBuilder.fromUri(deviceLinkBase).queryParam("deviceLinkType", deviceLinkType.getValue()); + addElapsedSecondsIfQrCode(uriBuilder); + uriBuilder.queryParam("sessionToken", sessionToken).queryParam("sessionType", sessionType.getValue()) + .queryParam("version", version).queryParam("lang", lang); + return uriBuilder.build(); + } + + /** + * Builds the final Smart-ID device link URI by combining unprotected link and authCode. + * + * @param sessionSecret session secret received from session initialization response. + * @return full device link URI with authCode parameter + */ + public URI buildDeviceLink(String sessionSecret) { + URI unprotectedUri = createUnprotectedUri(); + String authCode = generateAuthCode(unprotectedUri.toString(), sessionSecret); + return UriBuilder.fromUri(unprotectedUri) + .queryParam("authCode", authCode) + .build(); + } + + private void addElapsedSecondsIfQrCode(UriBuilder uriBuilder) { + if (elapsedSeconds != null) { + if (deviceLinkType != DeviceLinkType.QR_CODE) { + throw new SmartIdClientException("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE"); + } + uriBuilder.queryParam("elapsedSeconds", elapsedSeconds); + } + } + + private String generateAuthCode(String unprotectedLink, String sessionSecret) { + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter 'sessionSecret' cannot be empty"); + } + validateAuthCodeParams(); + return calculateAuthCode(buildPayload(unprotectedLink), sessionSecret); + } + + private String buildPayload(String unprotectedLink) { + return String.join("|", + schemeName, + getSignatureProtocolForSession(), + StringUtil.orEmpty(digest), + relyingPartyNameBase64, + StringUtil.orEmpty(brokeredRpNameBase64), + StringUtil.orEmpty(interactions), + StringUtil.orEmpty(initialCallbackUrl), + unprotectedLink + ); + } + + private String getSignatureProtocolForSession() { + return switch (sessionType) { + case AUTHENTICATION -> SignatureProtocol.ACSP_V2.name(); + case SIGNATURE -> SignatureProtocol.RAW_DIGEST_SIGNATURE.name(); + case CERTIFICATE_CHOICE -> ""; + }; + } + + private String calculateAuthCode(String data, String base64Key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(Base64.getDecoder().decode(base64Key), "HmacSHA256")); + byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hmac); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException ex) { + throw new SmartIdClientException("Failed to calculate authCode", ex); + } + } + + private void validateInputParameters() { + if (StringUtil.isEmpty(deviceLinkBase)) { + throw new SmartIdClientException("Parameter 'deviceLinkBase' cannot be empty"); + } + if (StringUtil.isEmpty(version)) { + throw new SmartIdClientException("Parameter 'version' cannot be empty"); + } + if (!ALLOWED_VERSION.equals(version)) { + throw new SmartIdClientException("Only version 1.0 is allowed"); + } + if (deviceLinkType == null) { + throw new SmartIdClientException("Parameter 'deviceLinkType' must be set"); + } + if (sessionType == null) { + throw new SmartIdClientException("Parameter 'sessionType' must be set"); + } + if (StringUtil.isEmpty(sessionToken)) { + throw new SmartIdClientException("Parameter 'sessionToken' cannot be empty"); + } + if (deviceLinkType == DeviceLinkType.QR_CODE && elapsedSeconds == null) { + throw new SmartIdClientException("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE"); + } + if (StringUtil.isEmpty(lang)) { + throw new SmartIdClientException("Parameter 'lang' must be set"); + } + } + + private void validateAuthCodeParams() { + if (StringUtil.isEmpty(schemeName)) { + throw new SmartIdClientException("Parameter 'schemeName' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyNameBase64)) { + throw new SmartIdClientException("Parameter 'relyingPartyName' cannot be empty"); + } + + boolean hasCallback = StringUtil.isNotEmpty(initialCallbackUrl); + if (deviceLinkType == DeviceLinkType.QR_CODE && hasCallback) { + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE"); + } + if ((deviceLinkType == DeviceLinkType.APP_2_APP || deviceLinkType == DeviceLinkType.WEB_2_APP) && !hasCallback) { + throw new SmartIdClientException("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP"); + } + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } + if (StringUtil.isEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE"); + } + } + if (sessionType == SessionType.CERTIFICATE_CHOICE) { + if (StringUtil.isNotEmpty(digest)) { + throw new SmartIdClientException("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } + if (StringUtil.isNotEmpty(interactions)) { + throw new SmartIdClientException("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE"); + } + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java index f1971dc2..423a189e 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilder.java @@ -1,212 +1,212 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static ee.sk.smartid.util.StringUtil.isEmpty; - -import java.util.Set; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder class for creating and initializing a device link-based certificate choice session. - */ -public class DeviceLinkCertificateChoiceSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private Boolean shareMdClientIpAddress; - private String initialCallbackUrl; - - /** - * Constructs a new DeviceLinkCertificateRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce - * - * @param nonce the nonce - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the initial callback URL for the device link session. - * This URL is used to redirect the user after the session is initialized. - * - * @param initialCallbackUrl the initial callback URL - * @return this builder - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Starts a device link-based certificate choice session and returns the session response. - * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, - * which can be used by the Relying Party to manage and verify the session independently. - * - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. - * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. - * @throws UnprocessableSmartIdResponseException if the response is missing required fields. - */ - public DeviceLinkSessionResponse initCertificateChoice() { - validateRequestParameters(); - DeviceLinkCertificateChoiceSessionRequest deviceLinkCertificateChoiceSessionRequest = createCertificateRequest(); - DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(deviceLinkCertificateChoiceSessionRequest); - validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); - return deviceLinkCertificateChoiceSessionResponse; - } - - private void validateRequestParameters() { - if (isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' must have length between 1 and 30 characters"); - } - validateInitialCallbackUrl(); - } - - private DeviceLinkCertificateChoiceSessionRequest createCertificateRequest() { - return new DeviceLinkCertificateChoiceSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - nonce, - capabilities, - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - initialCallbackUrl - ); - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); - } - - if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); - } - - if (deviceLinkCertificateChoiceSessionResponse.deviceLinkBase() == null - || deviceLinkCertificateChoiceSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static ee.sk.smartid.util.StringUtil.isEmpty; + +import java.util.Set; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder class for creating and initializing a device link-based certificate choice session. + */ +public class DeviceLinkCertificateChoiceSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private Boolean shareMdClientIpAddress; + private String initialCallbackUrl; + + /** + * Constructs a new DeviceLinkCertificateRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the initial callback URL for the device link session. + * This URL is used to redirect the user after the session is initialized. + * + * @param initialCallbackUrl the initial callback URL + * @return this builder + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Starts a device link-based certificate choice session and returns the session response. + * This response includes essential values such as sessionID, sessionToken, sessionSecret and deviceLinkBase URL, + * which can be used by the Relying Party to manage and verify the session independently. + * + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL for further session management. + * @throws SmartIdRequestSetupException if the request is invalid or missing necessary data. + * @throws UnprocessableSmartIdResponseException if the response is missing required fields. + */ + public DeviceLinkSessionResponse initCertificateChoice() { + validateRequestParameters(); + DeviceLinkCertificateChoiceSessionRequest deviceLinkCertificateChoiceSessionRequest = createCertificateRequest(); + DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse = connector.initDeviceLinkCertificateChoice(deviceLinkCertificateChoiceSessionRequest); + validateResponseParameters(deviceLinkCertificateChoiceSessionResponse); + return deviceLinkCertificateChoiceSessionResponse; + } + + private void validateRequestParameters() { + if (isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must have length between 1 and 30 characters"); + } + validateInitialCallbackUrl(); + } + + private DeviceLinkCertificateChoiceSessionRequest createCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl + ); + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private static void validateResponseParameters(DeviceLinkSessionResponse deviceLinkCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionID' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty"); + } + + if (StringUtil.isEmpty(deviceLinkCertificateChoiceSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty"); + } + + if (deviceLinkCertificateChoiceSessionResponse.deviceLinkBase() == null + || deviceLinkCertificateChoiceSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java index 8a997ec5..56687739 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilder.java @@ -1,356 +1,356 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a device-link signature session - */ -public class DeviceLinkSignatureSessionRequestBuilder { - - private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private SemanticsIdentifier semanticsIdentifier; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private List interactions; - private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private String initialCallbackUrl; - private DigestInput digestInput; - - private DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest; - - /** - * Constructs a new Smart-ID signature request builder with the given connector. - * - * @param connector the connector - */ - public DeviceLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the semantics identifier. - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the interactions for device-link signature. - * - * @param interactions the interactions - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the data to be signed. - *

- * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - * - * @param signableData the data to be signed - * @return this builder instance - */ - public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (this.digestInput != null && this.digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash."); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the hash to be signed in the signature protocol. - *

- * The provided {@link SignableHash} must contain a valid hash value and hash type, - * which will be used as the digest in the signing request. - * - * @param signableHash the hash data to be signed - * @return this builder - */ - public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (this.digestInput != null && this.digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData."); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sets the initial callback URL. - *

- * This URL is used to redirect the user after the signature session is completed. - * - * @param initialCallbackUrl the initial callback URL - * @return this builder instance - */ - public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { - this.initialCallbackUrl = initialCallbackUrl; - return this; - } - - /** - * Sends the signature request and initiates a device link based signature session. - *

- * There are two supported ways to start the signature session: - *

    - *
  • with a document number by using {@link #withDocumentNumber(String)}
  • - *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
- * - * @return a {@link DeviceLinkSessionResponse} containing session details such as - * session ID, session token, session secret and device link base URL. - * @throws SmartIdRequestSetupException if request parameters are invalid - * @throws SmartIdClientException if digest calculation fails or if both signableData and signableHash are missing - * @throws UnprocessableSmartIdResponseException if the response is missing required fields - */ - public DeviceLinkSessionResponse initSignatureSession() { - validateRequestParameters(); - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = createSignatureSessionRequest(); - DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(deviceLinkSignatureSessionRequest); - validateResponseParameters(deviceLinkSignatureSessionResponse); - this.deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequest; - return deviceLinkSignatureSessionResponse; - } - - /** - * Gets the DeviceLinkSignatureSessionRequest that was used to initiate the signature session. - *

- * This method can only be called after {@link #initSignatureSession()} has been invoked. - * - * @return the signature request that was used to initiate the session - * @throws SmartIdClientException if called before initSignatureSession() - */ - public DeviceLinkSignatureSessionRequest getSignatureSessionRequest() { - if (deviceLinkSignatureSessionRequest == null) { - throw new SmartIdClientException("Signature session has not been initiated yet"); - } - return deviceLinkSignatureSessionRequest; - } - - private DeviceLinkSessionResponse initSignatureSession(DeviceLinkSignatureSessionRequest request) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (!StringUtil.isEmpty(documentNumber)) { - return connector.initDeviceLinkSignature(request, documentNumber); - } else if (semanticsIdentifier != null) { - return connector.initDeviceLinkSignature(request, semanticsIdentifier); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"); - } - } - - private DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new DeviceLinkSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - nonce != null ? nonce : null, - capabilities, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - initialCallbackUrl); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); - } - validateInteractions(); - validateInitialCallbackUrl(); - - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters."); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateInitialCallbackUrl() { - if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { - throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); - } - } - - private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); - } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); - } - if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); - } - if (deviceLinkSignatureSessionResponse.deviceLinkBase() == null || deviceLinkSignatureSessionResponse.deviceLinkBase().toString().isBlank()) { - throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); - } - } - +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a device-link signature session + */ +public class DeviceLinkSignatureSessionRequestBuilder { + + private static final String INITIAL_CALLBACK_URL_PATTERN = "^https://[^|]+$"; + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List interactions; + private Boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private String initialCallbackUrl; + private DigestInput digestInput; + + private DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public DeviceLinkSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the interactions for device-link signature. + * + * @param interactions the interactions + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

+ * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + * + * @param signableData the data to be signed + * @return this builder instance + */ + public DeviceLinkSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash."); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

+ * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + * + * @param signableHash the hash data to be signed + * @return this builder + */ + public DeviceLinkSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData."); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sets the initial callback URL. + *

+ * This URL is used to redirect the user after the signature session is completed. + * + * @param initialCallbackUrl the initial callback URL + * @return this builder instance + */ + public DeviceLinkSignatureSessionRequestBuilder withInitialCallbackUrl(String initialCallbackUrl) { + this.initialCallbackUrl = initialCallbackUrl; + return this; + } + + /** + * Sends the signature request and initiates a device link based signature session. + *

+ * There are two supported ways to start the signature session: + *

    + *
  • with a document number by using {@link #withDocumentNumber(String)}
  • + *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
+ * + * @return a {@link DeviceLinkSessionResponse} containing session details such as + * session ID, session token, session secret and device link base URL. + * @throws SmartIdRequestSetupException if request parameters are invalid + * @throws SmartIdClientException if digest calculation fails or if both signableData and signableHash are missing + * @throws UnprocessableSmartIdResponseException if the response is missing required fields + */ + public DeviceLinkSessionResponse initSignatureSession() { + validateRequestParameters(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = createSignatureSessionRequest(); + DeviceLinkSessionResponse deviceLinkSignatureSessionResponse = initSignatureSession(deviceLinkSignatureSessionRequest); + validateResponseParameters(deviceLinkSignatureSessionResponse); + this.deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequest; + return deviceLinkSignatureSessionResponse; + } + + /** + * Gets the DeviceLinkSignatureSessionRequest that was used to initiate the signature session. + *

+ * This method can only be called after {@link #initSignatureSession()} has been invoked. + * + * @return the signature request that was used to initiate the session + * @throws SmartIdClientException if called before initSignatureSession() + */ + public DeviceLinkSignatureSessionRequest getSignatureSessionRequest() { + if (deviceLinkSignatureSessionRequest == null) { + throw new SmartIdClientException("Signature session has not been initiated yet"); + } + return deviceLinkSignatureSessionRequest; + } + + private DeviceLinkSessionResponse initSignatureSession(DeviceLinkSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (!StringUtil.isEmpty(documentNumber)) { + return connector.initDeviceLinkSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initDeviceLinkSignature(request, semanticsIdentifier); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed"); + } + } + + private DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + return new DeviceLinkSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce != null ? nonce : null, + capabilities, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + initialCallbackUrl); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } + validateInteractions(); + validateInitialCallbackUrl(); + + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters."); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateInitialCallbackUrl() { + if (!StringUtil.isEmpty(initialCallbackUrl) && !initialCallbackUrl.matches(INITIAL_CALLBACK_URL_PATTERN)) { + throw new SmartIdRequestSetupException("Value for 'initialCallbackUrl' must match pattern " + INITIAL_CALLBACK_URL_PATTERN + " and must not contain unencoded vertical bars"); + } + } + + private void validateResponseParameters(DeviceLinkSessionResponse deviceLinkSignatureSessionResponse) { + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionID' is missing or empty"); + } + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionToken())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionToken' is missing or empty"); + } + if (StringUtil.isEmpty(deviceLinkSignatureSessionResponse.sessionSecret())) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'sessionSecret' is missing or empty"); + } + if (deviceLinkSignatureSessionResponse.deviceLinkBase() == null || deviceLinkSignatureSessionResponse.deviceLinkBase().toString().isBlank()) { + throw new UnprocessableSmartIdResponseException("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty"); + } + } + } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/DeviceLinkType.java b/src/main/java/ee/sk/smartid/DeviceLinkType.java index efd71eed..0f8448aa 100644 --- a/src/main/java/ee/sk/smartid/DeviceLinkType.java +++ b/src/main/java/ee/sk/smartid/DeviceLinkType.java @@ -1,63 +1,63 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Representation types for different device link flows. - */ -public enum DeviceLinkType { - - /** - * QR-code (cross-device) flow. - */ - QR_CODE("QR"), - - /** - * Web2App (same-device) flow. - */ - WEB_2_APP("Web2App"), - - /** - * App2App (same-device) flow. - */ - APP_2_APP("App2App"); - - private final String value; - - DeviceLinkType(String value) { - this.value = value; - } - - /** - * Provides the device link type as value that can be used in the Smart-ID API - * - * @return code representing the device link type - */ - public String getValue() { - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Representation types for different device link flows. + */ +public enum DeviceLinkType { + + /** + * QR-code (cross-device) flow. + */ + QR_CODE("QR"), + + /** + * Web2App (same-device) flow. + */ + WEB_2_APP("Web2App"), + + /** + * App2App (same-device) flow. + */ + APP_2_APP("App2App"); + + private final String value; + + DeviceLinkType(String value) { + this.value = value; + } + + /** + * Provides the device link type as value that can be used in the Smart-ID API + * + * @return code representing the device link type + */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/DigestCalculator.java b/src/main/java/ee/sk/smartid/DigestCalculator.java index 28e067f1..a8ace55e 100644 --- a/src/main/java/ee/sk/smartid/DigestCalculator.java +++ b/src/main/java/ee/sk/smartid/DigestCalculator.java @@ -1,61 +1,61 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for calculating digests using specified hash algorithms. - */ -public final class DigestCalculator { - - private DigestCalculator() { - } - - /** - * Calculates the digest of the provided data using the specified hash algorithm. - * - * @param dataToDigest The data to be hashed. - * @param hashAlgorithm The hash algorithm to use. - * @return The calculated digest as a byte array. - * @throws SmartIdClientException If there is an issue with the digest calculation. - */ - public static byte[] calculateDigest(byte[] dataToDigest, HashAlgorithm hashAlgorithm) { - if (hashAlgorithm == null) { - throw new SmartIdClientException("Parameter 'hashAlgorithm' must be set"); - } - try { - MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName()); - return digest.digest(dataToDigest); - } catch (NoSuchAlgorithmException ex) { - throw new SmartIdClientException("Problem with digest calculation.", ex); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for calculating digests using specified hash algorithms. + */ +public final class DigestCalculator { + + private DigestCalculator() { + } + + /** + * Calculates the digest of the provided data using the specified hash algorithm. + * + * @param dataToDigest The data to be hashed. + * @param hashAlgorithm The hash algorithm to use. + * @return The calculated digest as a byte array. + * @throws SmartIdClientException If there is an issue with the digest calculation. + */ + public static byte[] calculateDigest(byte[] dataToDigest, HashAlgorithm hashAlgorithm) { + if (hashAlgorithm == null) { + throw new SmartIdClientException("Parameter 'hashAlgorithm' must be set"); + } + try { + MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName()); + return digest.digest(dataToDigest); + } catch (NoSuchAlgorithmException ex) { + throw new SmartIdClientException("Problem with digest calculation.", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/DigestInput.java b/src/main/java/ee/sk/smartid/DigestInput.java index c2a7721a..bf74c745 100644 --- a/src/main/java/ee/sk/smartid/DigestInput.java +++ b/src/main/java/ee/sk/smartid/DigestInput.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Represents data to be signed. - *

- * Digest for signing can be provided either as a pre-calculated hash value {@link SignableHash} or as raw data to be hashed {@link SignableData}. - *

- * Implementers must make sure that the getDigestInBase64() method returns the digest of the data to be in Base64-encoded format and the hashAlgorithm() - * return the correct hash algorithm used for calculating the digest or to be used for hashing the raw data. - */ -public interface DigestInput { - - /** - * Gets the digest in Base64-encoded string. - * - * @return the digest in base64 encoding - */ - String getDigestInBase64(); - - /** - * Gets the hash algorithm used for calculating the digest or to be used for hashing the raw data. - * - * @return the hash algorithm - */ - HashAlgorithm hashAlgorithm(); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Represents data to be signed. + *

+ * Digest for signing can be provided either as a pre-calculated hash value {@link SignableHash} or as raw data to be hashed {@link SignableData}. + *

+ * Implementers must make sure that the getDigestInBase64() method returns the digest of the data to be in Base64-encoded format and the hashAlgorithm() + * return the correct hash algorithm used for calculating the digest or to be used for hashing the raw data. + */ +public interface DigestInput { + + /** + * Gets the digest in Base64-encoded string. + * + * @return the digest in base64 encoding + */ + String getDigestInBase64(); + + /** + * Gets the hash algorithm used for calculating the digest or to be used for hashing the raw data. + * + * @return the hash algorithm + */ + HashAlgorithm hashAlgorithm(); +} diff --git a/src/main/java/ee/sk/smartid/ErrorResultHandler.java b/src/main/java/ee/sk/smartid/ErrorResultHandler.java index d96e44df..26f323c1 100644 --- a/src/main/java/ee/sk/smartid/ErrorResultHandler.java +++ b/src/main/java/ee/sk/smartid/ErrorResultHandler.java @@ -1,97 +1,97 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.UserAccountException; -import ee.sk.smartid.exception.UserActionException; -import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; -import ee.sk.smartid.exception.permanent.ProtocolFailureException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdServerException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.util.StringUtil; - -/** - * Handles session status results that end as completed but with an error - */ -public class ErrorResultHandler { - - /** - * Handles the session result and throws an appropriate exception - * - * @param sessionResult the session result to handle - * @throws SmartIdClientException when input parameter sessionResult is null - * @throws UserActionException sub-exceptions based on end result - * @throws UserAccountException sub-exceptions based on end result - * @throws ProtocolFailureException when there was an error in the process (e.g. schema name is incorrect) - * @throws ExpectedLinkedSessionException when different session type was started than expected - * @throws SmartIdServerException when technical error occurred on server side - * @throws UnprocessableSmartIdResponseException when unexpected end result was received - */ - public static void handle(SessionResult sessionResult) { - if (sessionResult == null) { - throw new SmartIdClientException("Parameter 'sessionResult' is not provided"); - } - switch (sessionResult.getEndResult()) { - case "USER_REFUSED" -> throw new UserRefusedException(); - case "TIMEOUT" -> throw new SessionTimeoutException(); - case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); - case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); - case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); - case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); - case "USER_REFUSED_INTERACTION" -> handleUserRefusedInteraction(sessionResult); - case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); - case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); - case "SERVER_ERROR" -> throw new SmartIdServerException(); - case "ACCOUNT_UNUSABLE" -> throw new UserAccountUnusableException(); - default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); - } - } - - private static void handleUserRefusedInteraction(SessionResult sessionResult) { - if (sessionResult.getDetails() == null || StringUtil.isEmpty(sessionResult.getDetails().getInteraction())) { - throw new UnprocessableSmartIdResponseException("Details for refused interaction are missing"); - } - switch (sessionResult.getDetails().getInteraction()) { - case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); - case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); - case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); - default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.UserAccountException; +import ee.sk.smartid.exception.UserActionException; +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.util.StringUtil; + +/** + * Handles session status results that end as completed but with an error + */ +public class ErrorResultHandler { + + /** + * Handles the session result and throws an appropriate exception + * + * @param sessionResult the session result to handle + * @throws SmartIdClientException when input parameter sessionResult is null + * @throws UserActionException sub-exceptions based on end result + * @throws UserAccountException sub-exceptions based on end result + * @throws ProtocolFailureException when there was an error in the process (e.g. schema name is incorrect) + * @throws ExpectedLinkedSessionException when different session type was started than expected + * @throws SmartIdServerException when technical error occurred on server side + * @throws UnprocessableSmartIdResponseException when unexpected end result was received + */ + public static void handle(SessionResult sessionResult) { + if (sessionResult == null) { + throw new SmartIdClientException("Parameter 'sessionResult' is not provided"); + } + switch (sessionResult.getEndResult()) { + case "USER_REFUSED" -> throw new UserRefusedException(); + case "TIMEOUT" -> throw new SessionTimeoutException(); + case "DOCUMENT_UNUSABLE" -> throw new DocumentUnusableException(); + case "WRONG_VC" -> throw new UserSelectedWrongVerificationCodeException(); + case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP" -> throw new RequiredInteractionNotSupportedByAppException(); + case "USER_REFUSED_CERT_CHOICE" -> throw new UserRefusedCertChoiceException(); + case "USER_REFUSED_INTERACTION" -> handleUserRefusedInteraction(sessionResult); + case "PROTOCOL_FAILURE" -> throw new ProtocolFailureException(); + case "EXPECTED_LINKED_SESSION" -> throw new ExpectedLinkedSessionException(); + case "SERVER_ERROR" -> throw new SmartIdServerException(); + case "ACCOUNT_UNUSABLE" -> throw new UserAccountUnusableException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected session result: " + sessionResult.getEndResult()); + } + } + + private static void handleUserRefusedInteraction(SessionResult sessionResult) { + if (sessionResult.getDetails() == null || StringUtil.isEmpty(sessionResult.getDetails().getInteraction())) { + throw new UnprocessableSmartIdResponseException("Details for refused interaction are missing"); + } + switch (sessionResult.getDetails().getInteraction()) { + case "displayTextAndPIN" -> throw new UserRefusedDisplayTextAndPinException(); + case "confirmationMessage" -> throw new UserRefusedConfirmationMessageException(); + case "confirmationMessageAndVerificationCodeChoice" -> throw new UserRefusedConfirmationMessageWithVerificationChoiceException(); + default -> throw new UnprocessableSmartIdResponseException("Unexpected interaction type: " + sessionResult.getDetails().getInteraction()); + } + } +} diff --git a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java index 5d77135e..be68443e 100644 --- a/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java +++ b/src/main/java/ee/sk/smartid/FileTrustedCAStoreBuilder.java @@ -1,224 +1,224 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.PKIXCertPathValidatorResult; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for TrustedCACertStore that loads trust anchors and intermediate CA certificates from specified keystore files. - *

- * The builder allows configuration of trust anchor and intermediate CA keystore paths and passwords. - */ -public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { - - private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); - - private String trustAnchorTruststorePath = "/sid_trust_anchor_certificates.jks"; - private String trustAnchorTruststorePassword = "changeit"; - - private String intermediateCATruststorePath = "/trusted_certificates.jks"; - private String trustedCaTruststorePassword = "changeit"; - - private boolean ocspEnabled = false; // TODO - 03.07.25: set to true if OCSP validations is working - private X509Certificate ocspValidationCert; // TODO - 02.07.25: implement reading from a file system - - /** - * Sets the path to the trust anchor keystore file. - * - * @param path the path to the trust anchor keystore file - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withTrustAnchorTruststorePath(String path) { - this.trustAnchorTruststorePath = path; - return this; - } - - /** - * Sets the password for the trust anchor keystore. - * - * @param password the password for the trust anchor keystore - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withTrustAnchorTruststorePassword(String password) { - this.trustAnchorTruststorePassword = password; - return this; - } - - /** - * Sets the path to the intermediate CA keystore file. - * - * @param path the path to the trusted CA keystore file - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withIntermediateCATruststorePath(String path) { - this.intermediateCATruststorePath = path; - return this; - } - - /** - * Sets the password for the trusted CA keystore. - * - * @param password the password for the trusted CA keystore - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withIntermediateCATruststorePassword(String password) { - this.trustedCaTruststorePassword = password; - return this; - } - - /** - * Enables or disables OCSP (Online Certificate Status Protocol) for certificate validation. - * - * @param enabled true to enable OCSP, false to disable it - * @return this Builder instance - */ - public FileTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { - this.ocspEnabled = enabled; - return this; - } - - /** - * Builds a new TrustedCAStoreImpl instance with the specified configuration. - * - * @return a new TrustedCAStoreImpl instances - */ - @Override - public TrustedCACertStore build() { - if (!ocspEnabled) { - logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); - } else { - throw new UnsupportedOperationException("OCSP validation does not work yet, will be implemented later"); - } - Set trustAnchors = loadTrustAnchors(); - List trustedCACertificates = loadValidatedIntermediateCACertificates(trustAnchors); - return new DefaultTrustedCACertStore(trustAnchors, trustedCACertificates, ocspEnabled); - } - - private Set loadTrustAnchors() { - if (StringUtil.isEmpty(trustAnchorTruststorePath)) { - throw new SmartIdClientException("Trust anchor truststore path must be set"); - } - if (StringUtil.isEmpty(trustAnchorTruststorePassword)) { - throw new SmartIdClientException("Trust anchor truststore password must be set"); - } - try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(trustAnchorTruststorePath)) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, trustAnchorTruststorePassword.toCharArray()); - Enumeration aliases = keystore.aliases(); - Set trustAnchors = new HashSet<>(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - certificate.verify(certificate.getPublicKey()); - certificate.checkValidity(); - trustAnchors.add(new TrustAnchor(certificate, null)); - } - return trustAnchors; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing trust anchor certificate", e); - throw new SmartIdClientException("Error initializing trust anchor certificate", e); - } catch (SignatureException | InvalidKeyException | NoSuchProviderException ex) { - throw new SmartIdClientException("Failed to verify trust anchor certificate", ex); - } - } - - private List loadValidatedIntermediateCACertificates(Set trustAnchors) { - if (StringUtil.isEmpty(intermediateCATruststorePath)) { - throw new SmartIdClientException("Intermediate CA certificate truststore path must be set"); - } - if (StringUtil.isEmpty(trustedCaTruststorePassword)) { - throw new SmartIdClientException("Intermediate CA certificate truststore password must be set"); - } - try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(intermediateCATruststorePath)) { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, trustedCaTruststorePassword.toCharArray()); - Enumeration aliases = keystore.aliases(); - List trustedCACertificates = new ArrayList<>(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); - certificate.checkValidity(); - validateCertificate(trustAnchors, certificate); - trustedCACertificates.add(certificate); - } - return trustedCACertificates; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - logger.error("Error initializing intermediate CA certificates", e); - throw new SmartIdClientException("Error initializing intermediate CA certificates", e); - } - } - - private void validateCertificate(Set trustAnchors, X509Certificate x509Certificates) { - try { - var cf = CertificateFactory.getInstance("X.509"); - CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); - var pkixParameters = new PKIXParameters(trustAnchors); - pkixParameters.setRevocationEnabled(ocspEnabled); - var certPathValidator = CertPathValidator.getInstance("PKIX"); - var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); - var trustedCert = result.getTrustAnchor().getTrustedCert(); - logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); - } catch (GeneralSecurityException ex) { - logger.debug("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); - throw new SmartIdClientException("Validating intermediate CA failed", ex); - } - } - - private String getCNValue(X509Certificate certificate) { - String subjectDN = certificate.getSubjectX500Principal().getName(); - return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for TrustedCACertStore that loads trust anchors and intermediate CA certificates from specified keystore files. + *

+ * The builder allows configuration of trust anchor and intermediate CA keystore paths and passwords. + */ +public class FileTrustedCAStoreBuilder implements DefaultTrustedCACertStore.Builder { + + private static final Logger logger = LoggerFactory.getLogger(FileTrustedCAStoreBuilder.class); + + private String trustAnchorTruststorePath = "/sid_trust_anchor_certificates.jks"; + private String trustAnchorTruststorePassword = "changeit"; + + private String intermediateCATruststorePath = "/trusted_certificates.jks"; + private String trustedCaTruststorePassword = "changeit"; + + private boolean ocspEnabled = false; // TODO - 03.07.25: set to true if OCSP validations is working + private X509Certificate ocspValidationCert; // TODO - 02.07.25: implement reading from a file system + + /** + * Sets the path to the trust anchor keystore file. + * + * @param path the path to the trust anchor keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePath(String path) { + this.trustAnchorTruststorePath = path; + return this; + } + + /** + * Sets the password for the trust anchor keystore. + * + * @param password the password for the trust anchor keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withTrustAnchorTruststorePassword(String password) { + this.trustAnchorTruststorePassword = password; + return this; + } + + /** + * Sets the path to the intermediate CA keystore file. + * + * @param path the path to the trusted CA keystore file + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePath(String path) { + this.intermediateCATruststorePath = path; + return this; + } + + /** + * Sets the password for the trusted CA keystore. + * + * @param password the password for the trusted CA keystore + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withIntermediateCATruststorePassword(String password) { + this.trustedCaTruststorePassword = password; + return this; + } + + /** + * Enables or disables OCSP (Online Certificate Status Protocol) for certificate validation. + * + * @param enabled true to enable OCSP, false to disable it + * @return this Builder instance + */ + public FileTrustedCAStoreBuilder withOcspEnabled(boolean enabled) { + this.ocspEnabled = enabled; + return this; + } + + /** + * Builds a new TrustedCAStoreImpl instance with the specified configuration. + * + * @return a new TrustedCAStoreImpl instances + */ + @Override + public TrustedCACertStore build() { + if (!ocspEnabled) { + logger.warn("TrustedCAStore will be initialized with OCSP check disabled. This is not recommended for production use as it may lead to security vulnerabilities."); + } else { + throw new UnsupportedOperationException("OCSP validation does not work yet, will be implemented later"); + } + Set trustAnchors = loadTrustAnchors(); + List trustedCACertificates = loadValidatedIntermediateCACertificates(trustAnchors); + return new DefaultTrustedCACertStore(trustAnchors, trustedCACertificates, ocspEnabled); + } + + private Set loadTrustAnchors() { + if (StringUtil.isEmpty(trustAnchorTruststorePath)) { + throw new SmartIdClientException("Trust anchor truststore path must be set"); + } + if (StringUtil.isEmpty(trustAnchorTruststorePassword)) { + throw new SmartIdClientException("Trust anchor truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(trustAnchorTruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustAnchorTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + Set trustAnchors = new HashSet<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.verify(certificate.getPublicKey()); + certificate.checkValidity(); + trustAnchors.add(new TrustAnchor(certificate, null)); + } + return trustAnchors; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing trust anchor certificate", e); + throw new SmartIdClientException("Error initializing trust anchor certificate", e); + } catch (SignatureException | InvalidKeyException | NoSuchProviderException ex) { + throw new SmartIdClientException("Failed to verify trust anchor certificate", ex); + } + } + + private List loadValidatedIntermediateCACertificates(Set trustAnchors) { + if (StringUtil.isEmpty(intermediateCATruststorePath)) { + throw new SmartIdClientException("Intermediate CA certificate truststore path must be set"); + } + if (StringUtil.isEmpty(trustedCaTruststorePassword)) { + throw new SmartIdClientException("Intermediate CA certificate truststore password must be set"); + } + try (InputStream is = DefaultTrustedCACertStore.class.getResourceAsStream(intermediateCATruststorePath)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, trustedCaTruststorePassword.toCharArray()); + Enumeration aliases = keystore.aliases(); + List trustedCACertificates = new ArrayList<>(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + X509Certificate certificate = (X509Certificate) keystore.getCertificate(alias); + certificate.checkValidity(); + validateCertificate(trustAnchors, certificate); + trustedCACertificates.add(certificate); + } + return trustedCACertificates; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + logger.error("Error initializing intermediate CA certificates", e); + throw new SmartIdClientException("Error initializing intermediate CA certificates", e); + } + } + + private void validateCertificate(Set trustAnchors, X509Certificate x509Certificates) { + try { + var cf = CertificateFactory.getInstance("X.509"); + CertPath certPath = cf.generateCertPath(List.of(x509Certificates)); + var pkixParameters = new PKIXParameters(trustAnchors); + pkixParameters.setRevocationEnabled(ocspEnabled); + var certPathValidator = CertPathValidator.getInstance("PKIX"); + var result = (PKIXCertPathValidatorResult) certPathValidator.validate(certPath, pkixParameters); + var trustedCert = result.getTrustAnchor().getTrustedCert(); + logger.debug("Certificate '{}' was trusted by '{}'", getCNValue(x509Certificates), getCNValue(trustedCert)); + } catch (GeneralSecurityException ex) { + logger.debug("Validation of '{}' failed", x509Certificates.getSubjectX500Principal(), ex); + throw new SmartIdClientException("Validating intermediate CA failed", ex); + } + } + + private String getCNValue(X509Certificate certificate) { + String subjectDN = certificate.getSubjectX500Principal().getName(); + return CertificateAttributeUtil.getAttributeValue(subjectDN, BCStyle.CN).orElse(null); + } +} diff --git a/src/main/java/ee/sk/smartid/FlowType.java b/src/main/java/ee/sk/smartid/FlowType.java index 5ae94f56..984a5114 100644 --- a/src/main/java/ee/sk/smartid/FlowType.java +++ b/src/main/java/ee/sk/smartid/FlowType.java @@ -1,97 +1,97 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents the flow types that user used to complete the authentication or signing. - */ -public enum FlowType { - - /** - * QR-code (cross-device) flow. User scanned a QR-code with the Smart-ID app. - */ - QR("QR"), - - /** - * Web2App (same-device) flow. User clicked on a link in the browser on a mobile device - * and confirmed with the Smart-ID app. - */ - WEB2APP("Web2App"), - - /** - * App2App (same-device) flow. User clicked on a link in the app on a mobile device - * and confirmed with the Smart-ID app. - */ - APP2APP("App2App"), - - /** - * Notification flow. User received a push-notification and confirmed with the Smart-ID app. - */ - NOTIFICATION("Notification"); - - private final String description; - - FlowType(String description) { - this.description = description; - } - - /*** - * Gets the value used in the Smart-ID API to represent the flow type. - * - * @return the flow type description - */ - public String getDescription() { - return description; - } - - /** - * Checks if the provided flow type is supported. - * - * @param flowType the flow type to check - * @return true if the flow type is supported, false otherwise - */ - public static boolean isSupported(String flowType) { - return Arrays.stream(FlowType.values()) - .anyMatch(f -> f.getDescription().equals(flowType)); - } - - /** - * Converts a string representation of a flow type to the corresponding FlowType enum value. - * - * @param flowType the string representation of the flow type - * @return the corresponding FlowType enum value - * @throws IllegalArgumentException if the provided flow type is not valid - */ - public static FlowType fromString(String flowType) { - return Arrays.stream(FlowType.values()) - .filter(f -> f.getDescription().equals(flowType)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid flowType value: " + flowType)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents the flow types that user used to complete the authentication or signing. + */ +public enum FlowType { + + /** + * QR-code (cross-device) flow. User scanned a QR-code with the Smart-ID app. + */ + QR("QR"), + + /** + * Web2App (same-device) flow. User clicked on a link in the browser on a mobile device + * and confirmed with the Smart-ID app. + */ + WEB2APP("Web2App"), + + /** + * App2App (same-device) flow. User clicked on a link in the app on a mobile device + * and confirmed with the Smart-ID app. + */ + APP2APP("App2App"), + + /** + * Notification flow. User received a push-notification and confirmed with the Smart-ID app. + */ + NOTIFICATION("Notification"); + + private final String description; + + FlowType(String description) { + this.description = description; + } + + /*** + * Gets the value used in the Smart-ID API to represent the flow type. + * + * @return the flow type description + */ + public String getDescription() { + return description; + } + + /** + * Checks if the provided flow type is supported. + * + * @param flowType the flow type to check + * @return true if the flow type is supported, false otherwise + */ + public static boolean isSupported(String flowType) { + return Arrays.stream(FlowType.values()) + .anyMatch(f -> f.getDescription().equals(flowType)); + } + + /** + * Converts a string representation of a flow type to the corresponding FlowType enum value. + * + * @param flowType the string representation of the flow type + * @return the corresponding FlowType enum value + * @throws IllegalArgumentException if the provided flow type is not valid + */ + public static FlowType fromString(String flowType) { + return Arrays.stream(FlowType.values()) + .filter(f -> f.getDescription().equals(flowType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid flowType value: " + flowType)); + } +} diff --git a/src/main/java/ee/sk/smartid/HashAlgorithm.java b/src/main/java/ee/sk/smartid/HashAlgorithm.java index 959f337c..f7e74a9d 100644 --- a/src/main/java/ee/sk/smartid/HashAlgorithm.java +++ b/src/main/java/ee/sk/smartid/HashAlgorithm.java @@ -1,102 +1,102 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; -import java.util.Optional; - -/** - * Represents hash algorithms used for hashing the data to be signed. - *

- * * The algorithm name is the name used in the Smart-ID API. - * * The octet length represents salt length in bytes (octets) produced by the hash algorithm. - */ -public enum HashAlgorithm { - - /** - * SHA-256 - produces 32 bytes (256 bit) hash - */ - SHA_256("SHA-256", 32), - /** - * SHA-384 - produces 48 bytes (384 bit) hash - */ - SHA_384("SHA-384", 48), - /** - * SHA-384 - produces 64 bytes (512 bit) hash - */ - SHA_512("SHA-512", 64), - /** - * SHA3-256 - produces 32 bytes (256 bit) hash - */ - SHA3_256("SHA3-256", 32), - /** - * SHA3-384 - produces 48 bytes (384 bit) hash - */ - SHA3_384("SHA3-384", 48), - /** - * SHA3-384 - produces 64 bytes (512 bit) hash - */ - SHA3_512("SHA3-512", 64); - - private final String algorithmName; - private final int octetLength; - - HashAlgorithm(String algorithmName, int octetLength) { - this.algorithmName = algorithmName; - this.octetLength = octetLength; - } - - /** - * Gets the name of the algorithm as used in the Smart-ID API. - * - * @return the algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Gets the length of the hash in bytes (octets). - * - * @return the octet length - */ - public int getOctetLength() { - return octetLength; - } - - /** - * Find HashAlgorithm by its name. - * - * @param input the name of the algorithm - * @return an Optional containing the HashAlgorithm if found, or an empty Optional if not found - */ - public static Optional fromString(String input) { - return Arrays.stream(HashAlgorithm.values()) - .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) - .findFirst(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents hash algorithms used for hashing the data to be signed. + *

+ * * The algorithm name is the name used in the Smart-ID API. + * * The octet length represents salt length in bytes (octets) produced by the hash algorithm. + */ +public enum HashAlgorithm { + + /** + * SHA-256 - produces 32 bytes (256 bit) hash + */ + SHA_256("SHA-256", 32), + /** + * SHA-384 - produces 48 bytes (384 bit) hash + */ + SHA_384("SHA-384", 48), + /** + * SHA-384 - produces 64 bytes (512 bit) hash + */ + SHA_512("SHA-512", 64), + /** + * SHA3-256 - produces 32 bytes (256 bit) hash + */ + SHA3_256("SHA3-256", 32), + /** + * SHA3-384 - produces 48 bytes (384 bit) hash + */ + SHA3_384("SHA3-384", 48), + /** + * SHA3-384 - produces 64 bytes (512 bit) hash + */ + SHA3_512("SHA3-512", 64); + + private final String algorithmName; + private final int octetLength; + + HashAlgorithm(String algorithmName, int octetLength) { + this.algorithmName = algorithmName; + this.octetLength = octetLength; + } + + /** + * Gets the name of the algorithm as used in the Smart-ID API. + * + * @return the algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Gets the length of the hash in bytes (octets). + * + * @return the octet length + */ + public int getOctetLength() { + return octetLength; + } + + /** + * Find HashAlgorithm by its name. + * + * @param input the name of the algorithm + * @return an Optional containing the HashAlgorithm if found, or an empty Optional if not found + */ + public static Optional fromString(String input) { + return Arrays.stream(HashAlgorithm.values()) + .filter(algorithm -> algorithm.getAlgorithmName().equals(input)) + .findFirst(); + } +} diff --git a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java index 0d2ca035..67785957 100644 --- a/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilder.java @@ -1,280 +1,280 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for initializing a linked notification signature session request. - * Must follow an anonymous device link certificate choice session - */ -public class LinkedNotificationSignatureSessionRequestBuilder { - - private final SmartIdConnector smartIdConnector; - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private DigestInput digestInput; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private String linkedSessionID; - private List interactions; - private CertificateLevel certificateLevel; - private String nonce; - private Boolean shareIpAddress; - private Set capabilities; - - /** - * Initializes the builder with the given Smart ID connector. - * - * @param smartIdConnector the Smart-ID connector - */ - public LinkedNotificationSignatureSessionRequestBuilder(SmartIdConnector smartIdConnector) { - this.smartIdConnector = smartIdConnector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the signable data. - * - * @param signableData the data to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with SignableHash - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (digestInput != null && digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableHash"); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the signable hash. - * - * @param signableHash the hash to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with SignableData - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (digestInput != null && digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableData"); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm The signature algorithm - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the linked session ID. - * - * @param linkedSessionID the session ID from the device link certificate choice session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withLinkedSessionID(String linkedSessionID) { - this.linkedSessionID = linkedSessionID; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the interactions. - * - * @param interactions list of interactions to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the mobile device's IP address with the relying party. - * - * @param shareIpAddress true to share the IP address, false otherwise - * @return this - */ - public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareIpAddress) { - this.shareIpAddress = shareIpAddress; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities to be used in the signing session - * @return this builder - */ - public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Initializes the linked notification signature session. - * - * @return The linked signature session response - * @throws SmartIdRequestSetupException when any required parameter is missing or invalid - * @throws UnprocessableSmartIdResponseException when server response is missing required fields - */ - public LinkedSignatureSessionResponse initSignatureSession() { - validateRequestParameters(); - LinkedSignatureSessionRequest request = createSessionRequest(); - LinkedSignatureSessionResponse linkedSignatureSessionResponse = smartIdConnector.initLinkedNotificationSignature(request, documentNumber); - validateResponse(linkedSignatureSessionResponse); - return linkedSignatureSessionResponse; - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (StringUtil.isEmpty(documentNumber)) { - throw new SmartIdRequestSetupException("Value for 'documentNumber' cannot be empty"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (StringUtil.isEmpty(linkedSessionID)) { - throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' must be 1-30 characters long"); - } - if (interactions == null || interactions.isEmpty()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private LinkedSignatureSessionRequest createSessionRequest() { - var rawDigestParams = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - return new LinkedSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - rawDigestParams, - linkedSessionID, - nonce, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, - capabilities); - } - - private void validateResponse(LinkedSignatureSessionResponse linkedSignatureSessionResponse) { - if (StringUtil.isEmpty(linkedSignatureSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Linked notification-base signature session response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for initializing a linked notification signature session request. + * Must follow an anonymous device link certificate choice session + */ +public class LinkedNotificationSignatureSessionRequestBuilder { + + private final SmartIdConnector smartIdConnector; + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private DigestInput digestInput; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private String linkedSessionID; + private List interactions; + private CertificateLevel certificateLevel; + private String nonce; + private Boolean shareIpAddress; + private Set capabilities; + + /** + * Initializes the builder with the given Smart ID connector. + * + * @param smartIdConnector the Smart-ID connector + */ + public LinkedNotificationSignatureSessionRequestBuilder(SmartIdConnector smartIdConnector) { + this.smartIdConnector = smartIdConnector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the signable data. + * + * @param signableData the data to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableHash + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (digestInput != null && digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableHash"); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the signable hash. + * + * @param signableHash the hash to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with SignableData + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (digestInput != null && digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has been already set with SignableData"); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm The signature algorithm + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the linked session ID. + * + * @param linkedSessionID the session ID from the device link certificate choice session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withLinkedSessionID(String linkedSessionID) { + this.linkedSessionID = linkedSessionID; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the interactions. + * + * @param interactions list of interactions to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the mobile device's IP address with the relying party. + * + * @param shareIpAddress true to share the IP address, false otherwise + * @return this + */ + public LinkedNotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareIpAddress) { + this.shareIpAddress = shareIpAddress; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities to be used in the signing session + * @return this builder + */ + public LinkedNotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Initializes the linked notification signature session. + * + * @return The linked signature session response + * @throws SmartIdRequestSetupException when any required parameter is missing or invalid + * @throws UnprocessableSmartIdResponseException when server response is missing required fields + */ + public LinkedSignatureSessionResponse initSignatureSession() { + validateRequestParameters(); + LinkedSignatureSessionRequest request = createSessionRequest(); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = smartIdConnector.initLinkedNotificationSignature(request, documentNumber); + validateResponse(linkedSignatureSessionResponse); + return linkedSignatureSessionResponse; + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (StringUtil.isEmpty(documentNumber)) { + throw new SmartIdRequestSetupException("Value for 'documentNumber' cannot be empty"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with SignableData or with SignableHash"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (StringUtil.isEmpty(linkedSessionID)) { + throw new SmartIdRequestSetupException("Value for 'linkedSessionID' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' must be 1-30 characters long"); + } + if (interactions == null || interactions.isEmpty()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(DeviceLinkInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private LinkedSignatureSessionRequest createSessionRequest() { + var rawDigestParams = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + return new LinkedSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + rawDigestParams, + linkedSessionID, + nonce, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null, + capabilities); + } + + private void validateResponse(LinkedSignatureSessionResponse linkedSignatureSessionResponse) { + if (StringUtil.isEmpty(linkedSignatureSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Linked notification-base signature session response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java index d0800ca3..dad59496 100644 --- a/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/MaskGenAlgorithm.java @@ -1,80 +1,80 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Represents mask algorithm in the response and the value used in recrating the signature. - */ -public enum MaskGenAlgorithm { - - /** - * id-mgf1 is used in the Smart-ID API and MGF1 is the name used in the Java Cryptography API. - */ - ID_MGF1("id-mgf1", "MGF1"); - - private final String algorithmName; - private final String mgfName; - - MaskGenAlgorithm(String algorithmName, String mgfName) { - this.algorithmName = algorithmName; - this.mgfName = mgfName; - } - - /** - * Gets the algorithm name used by the Smart-ID API. - * - * @return the algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Gets the MGF name used in the Java Cryptography API. - * - * @return the MGF name - */ - public String getMgfName() { - return mgfName; - } - - /** - * Converts a string to the corresponding MaskGenAlgorithm enum value. - * - * @param maskGenAlgorithm the string representation of the mask generation algorithm - * @return the corresponding MaskGenAlgorithm enum value - * @throws IllegalArgumentException if the provided string does not match any enum value - */ - public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { - return Arrays.stream(MaskGenAlgorithm.values()) - .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid maskGenAlgorithm value: " + maskGenAlgorithm)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Represents mask algorithm in the response and the value used in recrating the signature. + */ +public enum MaskGenAlgorithm { + + /** + * id-mgf1 is used in the Smart-ID API and MGF1 is the name used in the Java Cryptography API. + */ + ID_MGF1("id-mgf1", "MGF1"); + + private final String algorithmName; + private final String mgfName; + + MaskGenAlgorithm(String algorithmName, String mgfName) { + this.algorithmName = algorithmName; + this.mgfName = mgfName; + } + + /** + * Gets the algorithm name used by the Smart-ID API. + * + * @return the algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Gets the MGF name used in the Java Cryptography API. + * + * @return the MGF name + */ + public String getMgfName() { + return mgfName; + } + + /** + * Converts a string to the corresponding MaskGenAlgorithm enum value. + * + * @param maskGenAlgorithm the string representation of the mask generation algorithm + * @return the corresponding MaskGenAlgorithm enum value + * @throws IllegalArgumentException if the provided string does not match any enum value + */ + public static MaskGenAlgorithm fromString(String maskGenAlgorithm) { + return Arrays.stream(MaskGenAlgorithm.values()) + .filter(m -> m.getAlgorithmName().equals(maskGenAlgorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid maskGenAlgorithm value: " + maskGenAlgorithm)); + } +} diff --git a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java index fd42eb28..52132b42 100644 --- a/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidator.java @@ -1,56 +1,56 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the signature certificate is a nonqualified Smart-ID certificate and can be used for digital signing. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile - */ -public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { - - @Override - public void validate(X509Certificate certificate) { - NonQualifiedSmartIdCertificateValidator.validate(certificate); - validateCertificateCanBeUsedForSigning(certificate); - } - - private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { - if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a nonqualified Smart-ID certificate and can be used for digital signing. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + */ +public class NonQualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + @Override + public void validate(X509Certificate certificate) { + NonQualifiedSmartIdCertificateValidator.validate(certificate); + validateCertificateCanBeUsedForSigning(certificate); + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java index 8051558b..0efb494e 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationResponseValidator.java @@ -1,189 +1,189 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; -import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Validates notification-based authentication session status - */ -public class NotificationAuthenticationResponseValidator { - - private final CertificateValidator certificateValidator; - private final AuthenticationResponseMapper authenticationResponseMapper; - private final SignatureValueValidator signatureValueValidator; - private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; - - /** - * Creates an instance of {@link NotificationAuthenticationResponseValidator} - * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @param authenticationResponseMapper the mapper to convert session status to authentication response - * @param signatureValueValidator validator used to verify the correctness of the authentication signature value - * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validators based on certificate level - */ - public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, - AuthenticationResponseMapper authenticationResponseMapper, - SignatureValueValidator signatureValueValidator, - AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.authenticationResponseMapper = authenticationResponseMapper; - this.signatureValueValidator = signatureValueValidator; - this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; - } - - /** - * Creates an instance of {@link NotificationAuthenticationResponseValidator} using {@link CertificateValidator} - * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} - * - * @param certificateValidator validator used to verify the authentication certificate is valid and trusted - * @return a new instance of {@link NotificationAuthenticationResponseValidator} - */ - public static NotificationAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { - return new NotificationAuthenticationResponseValidator(certificateValidator, - new AuthenticationResponseMapperImpl(), - new SignatureValueValidatorImpl(), - new AuthenticationCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - * - * @param sessionStatus the session status - * @param authenticationSessionRequest the authentication session request - * @param schemaName the schema name used in the QR-code or device link - * @return the authentication identity - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName) { - return validate(sessionStatus, authenticationSessionRequest, schemaName, null); - } - - /** - * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. - *

- * Should only be used for QR-code or notification-based authentication validation - * - * @param sessionStatus the authentication session status to be validated - * @param authenticationSessionRequest the authentication session request that was used to start the session - * @param schemaName the schema name used in the QR-code or device link - * @param brokeredRpName the brokered relying party name - * @return authentication identity containing details about the authenticated user - */ - public AuthenticationIdentity validate(SessionStatus sessionStatus, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - validateInputs(sessionStatus, authenticationSessionRequest, schemaName); - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); - validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); - } - - private void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - if (authenticationSessionRequest == null) { - throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); - } - if (StringUtil.isEmpty(schemaName)) { - throw new SmartIdClientException("Parameter 'schemaName' is not provided"); - } - } - - private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - validateCertificateLevel(authenticationResponse, requestedCertificateLevel); - certificateValidator.validate(authenticationResponse.getCertificate()); - AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = - authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); - authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); - } - - private AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { - return authenticationSessionRequest.certificateLevel() == null - ? AuthenticationCertificateLevel.QUALIFIED - : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); - } - - private void validateSignature(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); - signatureValueValidator.validate(authenticationResponse.getSignatureValue(), - payload, - authenticationResponse.getCertificate(), - authenticationResponse.getRsaSsaPssSignatureParameters()); - } - - private byte[] constructPayload(AuthenticationResponse authenticationResponse, - NotificationAuthenticationSessionRequest authenticationSessionRequest, - String schemaName, - String brokeredRpName) { - String[] payload = { - schemaName, - SignatureProtocol.ACSP_V2.name(), - authenticationResponse.getServerRandom(), - authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), - StringUtil.orEmpty(authenticationResponse.getUserChallenge()), - toBase64(authenticationSessionRequest.relyingPartyName()), - StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), - InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), - authenticationResponse.getInteractionTypeUsed(), - "", - authenticationResponse.getFlowType().getDescription() - }; - return String - .join("|", payload) - .getBytes(StandardCharsets.UTF_8); - } - - private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { - if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { - throw new CertificateLevelMismatchException(); - } - } - - private static String toBase64(String input) { - return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidator; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactory; +import ee.sk.smartid.auth.AuthenticationCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Validates notification-based authentication session status + */ +public class NotificationAuthenticationResponseValidator { + + private final CertificateValidator certificateValidator; + private final AuthenticationResponseMapper authenticationResponseMapper; + private final SignatureValueValidator signatureValueValidator; + private final AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory; + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} + * using {@link CertificateValidator}, {@link AuthenticationResponseMapper} and {@link SignatureValueValidator} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @param authenticationResponseMapper the mapper to convert session status to authentication response + * @param signatureValueValidator validator used to verify the correctness of the authentication signature value + * @param authenticationCertificatePurposeValidatorFactory factory to create purpose validators based on certificate level + */ + public NotificationAuthenticationResponseValidator(CertificateValidator certificateValidator, + AuthenticationResponseMapper authenticationResponseMapper, + SignatureValueValidator signatureValueValidator, + AuthenticationCertificatePurposeValidatorFactory authenticationCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.authenticationResponseMapper = authenticationResponseMapper; + this.signatureValueValidator = signatureValueValidator; + this.authenticationCertificatePurposeValidatorFactory = authenticationCertificatePurposeValidatorFactory; + } + + /** + * Creates an instance of {@link NotificationAuthenticationResponseValidator} using {@link CertificateValidator} + * and using default implementations of {@link AuthenticationResponseMapperImpl} and {@link SignatureValueValidatorImpl} + * + * @param certificateValidator validator used to verify the authentication certificate is valid and trusted + * @return a new instance of {@link NotificationAuthenticationResponseValidator} + */ + public static NotificationAuthenticationResponseValidator defaultSetupWithCertificateValidator(CertificateValidator certificateValidator) { + return new NotificationAuthenticationResponseValidator(certificateValidator, + new AuthenticationResponseMapperImpl(), + new SignatureValueValidatorImpl(), + new AuthenticationCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + * + * @param sessionStatus the session status + * @param authenticationSessionRequest the authentication session request + * @param schemaName the schema name used in the QR-code or device link + * @return the authentication identity + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName) { + return validate(sessionStatus, authenticationSessionRequest, schemaName, null); + } + + /** + * Validates the authentication session status and converts it to {@link AuthenticationIdentity}. + *

+ * Should only be used for QR-code or notification-based authentication validation + * + * @param sessionStatus the authentication session status to be validated + * @param authenticationSessionRequest the authentication session request that was used to start the session + * @param schemaName the schema name used in the QR-code or device link + * @param brokeredRpName the brokered relying party name + * @return authentication identity containing details about the authenticated user + */ + public AuthenticationIdentity validate(SessionStatus sessionStatus, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + validateInputs(sessionStatus, authenticationSessionRequest, schemaName); + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + validateCertificate(authenticationResponse, getRequestedCertificateLevel(authenticationSessionRequest)); + validateSignature(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + return AuthenticationIdentityMapper.from(authenticationResponse.getCertificate()); + } + + private void validateInputs(SessionStatus sessionStatus, NotificationAuthenticationSessionRequest authenticationSessionRequest, String schemaName) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + if (authenticationSessionRequest == null) { + throw new SmartIdClientException("Parameter 'authenticationSessionRequest' is not provided"); + } + if (StringUtil.isEmpty(schemaName)) { + throw new SmartIdClientException("Parameter 'schemaName' is not provided"); + } + } + + private void validateCertificate(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + validateCertificateLevel(authenticationResponse, requestedCertificateLevel); + certificateValidator.validate(authenticationResponse.getCertificate()); + AuthenticationCertificatePurposeValidator authenticationCertificatePurposeValidator = + authenticationCertificatePurposeValidatorFactory.create(authenticationResponse.getCertificateLevel()); + authenticationCertificatePurposeValidator.validate(authenticationResponse.getCertificate()); + } + + private AuthenticationCertificateLevel getRequestedCertificateLevel(NotificationAuthenticationSessionRequest authenticationSessionRequest) { + return authenticationSessionRequest.certificateLevel() == null + ? AuthenticationCertificateLevel.QUALIFIED + : AuthenticationCertificateLevel.valueOf(authenticationSessionRequest.certificateLevel()); + } + + private void validateSignature(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + byte[] payload = constructPayload(authenticationResponse, authenticationSessionRequest, schemaName, brokeredRpName); + signatureValueValidator.validate(authenticationResponse.getSignatureValue(), + payload, + authenticationResponse.getCertificate(), + authenticationResponse.getRsaSsaPssSignatureParameters()); + } + + private byte[] constructPayload(AuthenticationResponse authenticationResponse, + NotificationAuthenticationSessionRequest authenticationSessionRequest, + String schemaName, + String brokeredRpName) { + String[] payload = { + schemaName, + SignatureProtocol.ACSP_V2.name(), + authenticationResponse.getServerRandom(), + authenticationSessionRequest.signatureProtocolParameters().rpChallenge(), + StringUtil.orEmpty(authenticationResponse.getUserChallenge()), + toBase64(authenticationSessionRequest.relyingPartyName()), + StringUtil.isEmpty(brokeredRpName) ? "" : toBase64(brokeredRpName), + InteractionUtil.calculateDigest(authenticationSessionRequest.interactions()), + authenticationResponse.getInteractionTypeUsed(), + "", + authenticationResponse.getFlowType().getDescription() + }; + return String + .join("|", payload) + .getBytes(StandardCharsets.UTF_8); + } + + private static void validateCertificateLevel(AuthenticationResponse authenticationResponse, AuthenticationCertificateLevel requestedCertificateLevel) { + if (!authenticationResponse.getCertificateLevel().isSameLevelOrHigher(requestedCertificateLevel)) { + throw new CertificateLevelMismatchException(); + } + } + + private static String toBase64(String input) { + return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8)); + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java index faeb4a4a..b694ffb5 100644 --- a/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilder.java @@ -1,315 +1,315 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; -import java.util.List; -import java.util.Set; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a notification-based authentication session - */ -public class NotificationAuthenticationSessionRequestBuilder { - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private AuthenticationCertificateLevel certificateLevel; - private String rpChallenge; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; - private List interactions; - private Boolean shareMdClientIpAddress; - private Set capabilities; - private SemanticsIdentifier semanticsIdentifier; - private String documentNumber; - - private NotificationAuthenticationSessionRequest notificationAuthenticationSessionRequest; - - /** - * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public NotificationAuthenticationSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartUUID the relying party UUID - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { - this.relyingPartyUUID = relyingPartUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the RP challenge - *

- * The provided rpChallenge must be a Base64 encoded string - *

- * Use {@link ee.sk.smartid.RpChallengeGenerator#generate()} to generate a valid RP challenge - * - * @param rpChallenge RP challenge in Base64 encoded format - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { - this.rpChallenge = rpChallenge; - return this; - } - - /** - * Sets the signature algorithm - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the hash algorithm - * - * @param hashAlgorithm the hash algorithm - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return this; - } - - /** - * Sets the interactions - * - * @param interactions the notification interactions - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the authentication session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the document number - *

- * Setting this value will make the authentication session request use the document number - * - * @param documentNumber the document number - * @return this builder - */ - public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sends the authentication request and get the init session response - *

- * There are 2 supported ways to start authentication session: - *

    - *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
  • with document number by using {@link #withDocumentNumber(String)}
  • - *
- * - * @return init session response - */ - public NotificationAuthenticationSessionResponse initAuthenticationSession() { - validateRequestParameters(); - NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); - NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); - validateResponseParameters(notificationAuthenticationSessionResponse); - this.notificationAuthenticationSessionRequest = authenticationRequest; - return notificationAuthenticationSessionResponse; - } - - /** - * Returns the built authentication session request - * - * @return the built authentication session request - */ - public NotificationAuthenticationSessionRequest getAuthenticationSessionRequest() { - if (notificationAuthenticationSessionRequest == null) { - throw new SmartIdClientException("Notification-based authentication session has not been initialized yet"); - } - return notificationAuthenticationSessionRequest; - } - - private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } else if (semanticsIdentifier != null) { - return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); - } else if (documentNumber != null) { - return connector.initNotificationAuthentication(authenticationRequest, documentNumber); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); - } - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - validateSignatureParameters(); - validateInteractions(); - } - - private void validateSignatureParameters() { - if (StringUtil.isEmpty(rpChallenge)) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); - } - try { - Base64.getDecoder().decode(rpChallenge); - } catch (IllegalArgumentException e) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); - } - if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { - throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private NotificationAuthenticationSessionRequest createAuthenticationRequest() { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(hashAlgorithm.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2.name(), - signatureProtocolParameters, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, - capabilities, - VerificationCodeType.NUMERIC4.getValue() - ); - } - - private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { - if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based authentication session initialisation response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; +import java.util.List; +import java.util.Set; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a notification-based authentication session + */ +public class NotificationAuthenticationSessionRequestBuilder { + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private AuthenticationCertificateLevel certificateLevel; + private String rpChallenge; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA3_512; + private List interactions; + private Boolean shareMdClientIpAddress; + private Set capabilities; + private SemanticsIdentifier semanticsIdentifier; + private String documentNumber; + + private NotificationAuthenticationSessionRequest notificationAuthenticationSessionRequest; + + /** + * Constructs a new NotificationAuthenticationSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationAuthenticationSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartUUID the relying party UUID + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyUUID(String relyingPartUUID) { + this.relyingPartyUUID = relyingPartUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCertificateLevel(AuthenticationCertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the RP challenge + *

+ * The provided rpChallenge must be a Base64 encoded string + *

+ * Use {@link ee.sk.smartid.RpChallengeGenerator#generate()} to generate a valid RP challenge + * + * @param rpChallenge RP challenge in Base64 encoded format + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withRpChallenge(String rpChallenge) { + this.rpChallenge = rpChallenge; + return this; + } + + /** + * Sets the signature algorithm + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the hash algorithm + * + * @param hashAlgorithm the hash algorithm + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withHashAlgorithm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + /** + * Sets the interactions + * + * @param interactions the notification interactions + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the authentication session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the document number + *

+ * Setting this value will make the authentication session request use the document number + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationAuthenticationSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sends the authentication request and get the init session response + *

+ * There are 2 supported ways to start authentication session: + *

    + *
  • with semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
  • with document number by using {@link #withDocumentNumber(String)}
  • + *
+ * + * @return init session response + */ + public NotificationAuthenticationSessionResponse initAuthenticationSession() { + validateRequestParameters(); + NotificationAuthenticationSessionRequest authenticationRequest = createAuthenticationRequest(); + NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse = initAuthenticationSession(authenticationRequest); + validateResponseParameters(notificationAuthenticationSessionResponse); + this.notificationAuthenticationSessionRequest = authenticationRequest; + return notificationAuthenticationSessionResponse; + } + + /** + * Returns the built authentication session request + * + * @return the built authentication session request + */ + public NotificationAuthenticationSessionRequest getAuthenticationSessionRequest() { + if (notificationAuthenticationSessionRequest == null) { + throw new SmartIdClientException("Notification-based authentication session has not been initialized yet"); + } + return notificationAuthenticationSessionRequest; + } + + private NotificationAuthenticationSessionResponse initAuthenticationSession(NotificationAuthenticationSessionRequest authenticationRequest) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } else if (semanticsIdentifier != null) { + return connector.initNotificationAuthentication(authenticationRequest, semanticsIdentifier); + } else if (documentNumber != null) { + return connector.initNotificationAuthentication(authenticationRequest, documentNumber); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); + } + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + validateSignatureParameters(); + validateInteractions(); + } + + private void validateSignatureParameters() { + if (StringUtil.isEmpty(rpChallenge)) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' cannot be empty"); + } + try { + Base64.getDecoder().decode(rpChallenge); + } catch (IllegalArgumentException e) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must be Base64-encoded string", e); + } + if (rpChallenge.length() < 44 || rpChallenge.length() > 88) { + throw new SmartIdRequestSetupException("Value for 'rpChallenge' must have length between 44 and 88 characters"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'hashAlgorithm' must be set"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private NotificationAuthenticationSessionRequest createAuthenticationRequest() { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters(rpChallenge, + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(hashAlgorithm.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2.name(), + signatureProtocolParameters, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null, + capabilities, + VerificationCodeType.NUMERIC4.getValue() + ); + } + + private void validateResponseParameters(NotificationAuthenticationSessionResponse notificationAuthenticationSessionResponse) { + if (StringUtil.isEmpty(notificationAuthenticationSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based authentication session initialisation response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java index 908a8d70..09dab6f5 100644 --- a/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilder.java @@ -1,195 +1,195 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for notification-based certificate choice session requests - */ -public class NotificationCertificateChoiceSessionRequestBuilder { - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private Boolean shareMdClientIpAddress; - private SemanticsIdentifier semanticsIdentifier; - - /** - * Constructs a new NotificationCertificateChoiceSessionRequestBuilder with the given Smart-ID connector - * - * @param connector the Smart-ID connector - */ - public NotificationCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the certificate level - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce - * - * @param nonce the nonce - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the semantics identifier - *

- * Setting this value will make the notification session request use the semantics identifier - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Initializes a notification-based certificate choice session - * - * @return init session response - * @throws SmartIdRequestSetupException whe the provided request parameters are invalid - * @throws UnprocessableSmartIdResponseException when the response is missing required parameters - * @throws SmartIdClientException when the request could not be sent - */ - public NotificationCertificateChoiceSessionResponse initCertificateChoice() { - validateRequestParameters(); - NotificationCertificateChoiceSessionRequest request = createCertificateChoiceRequest(); - NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); - validateResponseParameters(notificationCertificateChoiceSessionResponse); - return notificationCertificateChoiceSessionResponse; - } - - private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(NotificationCertificateChoiceSessionRequest request) { - if (semanticsIdentifier == null) { - throw new SmartIdRequestSetupException("Value for 'semanticIdentifier' must be set"); - } - return connector.initNotificationCertificateChoice(request, semanticsIdentifier); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); - } - } - - private NotificationCertificateChoiceSessionRequest createCertificateChoiceRequest() { - return new NotificationCertificateChoiceSessionRequest( - relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - nonce, - capabilities, - shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null); - } - - private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { - if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for notification-based certificate choice session requests + */ +public class NotificationCertificateChoiceSessionRequestBuilder { + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private Boolean shareMdClientIpAddress; + private SemanticsIdentifier semanticsIdentifier; + + /** + * Constructs a new NotificationCertificateChoiceSessionRequestBuilder with the given Smart-ID connector + * + * @param connector the Smart-ID connector + */ + public NotificationCertificateChoiceSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the certificate level + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce + * + * @param nonce the nonce + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the semantics identifier + *

+ * Setting this value will make the notification session request use the semantics identifier + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationCertificateChoiceSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Initializes a notification-based certificate choice session + * + * @return init session response + * @throws SmartIdRequestSetupException whe the provided request parameters are invalid + * @throws UnprocessableSmartIdResponseException when the response is missing required parameters + * @throws SmartIdClientException when the request could not be sent + */ + public NotificationCertificateChoiceSessionResponse initCertificateChoice() { + validateRequestParameters(); + NotificationCertificateChoiceSessionRequest request = createCertificateChoiceRequest(); + NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse = initCertificateChoiceSession(request); + validateResponseParameters(notificationCertificateChoiceSessionResponse); + return notificationCertificateChoiceSessionResponse; + } + + private NotificationCertificateChoiceSessionResponse initCertificateChoiceSession(NotificationCertificateChoiceSessionRequest request) { + if (semanticsIdentifier == null) { + throw new SmartIdRequestSetupException("Value for 'semanticIdentifier' must be set"); + } + return connector.initNotificationCertificateChoice(request, semanticsIdentifier); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); + } + } + + private NotificationCertificateChoiceSessionRequest createCertificateChoiceRequest() { + return new NotificationCertificateChoiceSessionRequest( + relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + nonce, + capabilities, + shareMdClientIpAddress != null ? new RequestProperties(shareMdClientIpAddress) : null); + } + + private void validateResponseParameters(NotificationCertificateChoiceSessionResponse notificationCertificateChoiceSessionResponse) { + if (StringUtil.isEmpty(notificationCertificateChoiceSessionResponse.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based certificate choice response field 'sessionID' is missing or empty"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java index 19295f5a..ead4a53b 100644 --- a/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java +++ b/src/main/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilder.java @@ -1,338 +1,338 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.common.InteractionsMapper; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.VerificationCode; -import ee.sk.smartid.util.InteractionUtil; -import ee.sk.smartid.util.SetUtil; -import ee.sk.smartid.util.StringUtil; - -/** - * Builder for creating a notification-based signature session - */ -public class NotificationSignatureSessionRequestBuilder { - - private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); - - private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[0-9]{4}$"); - - private final SmartIdConnector connector; - - private String relyingPartyUUID; - private String relyingPartyName; - private String documentNumber; - private SemanticsIdentifier semanticsIdentifier; - private CertificateLevel certificateLevel; - private String nonce; - private Set capabilities; - private List interactions; - private Boolean shareMdClientIpAddress; - private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - private DigestInput digestInput; - - /** - * Constructs a new Smart-ID signature request builder with the given connector. - * - * @param connector the connector - */ - public NotificationSignatureSessionRequestBuilder(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Sets the relying party UUID. - * - * @param relyingPartyUUID the relying party UUID - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - return this; - } - - /** - * Sets the relying party name. - * - * @param relyingPartyName the relying party name - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - return this; - } - - /** - * Sets the document number. - * - * @param documentNumber the document number - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - return this; - } - - /** - * Sets the semantics identifier. - * - * @param semanticsIdentifier the semantics identifier - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { - this.semanticsIdentifier = semanticsIdentifier; - return this; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - return this; - } - - /** - * Sets the nonce. - * - * @param nonce the nonce - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { - this.nonce = nonce; - return this; - } - - /** - * Sets the capabilities. - * - * @param capabilities the capabilities - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { - this.capabilities = SetUtil.toSet(capabilities); - return this; - } - - /** - * Sets the interactions. - * - * @param interactions the allowed interactions order - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withInteractions(List interactions) { - this.interactions = interactions; - return this; - } - - /** - * Sets whether to share the Mobile device IP address - * - * @param shareMdClientIpAddress whether to share the Mobile device IP address - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { - this.shareMdClientIpAddress = shareMdClientIpAddress; - return this; - } - - /** - * Sets the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - * @return this builder - */ - public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - return this; - } - - /** - * Sets the data to be signed. - *

- * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. - *

- * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. - * - * @param signableData the data to be signed - * @return this builder instance - * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableHash} - */ - public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { - if (this.digestInput != null && this.digestInput instanceof SignableHash) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash"); - } - this.digestInput = signableData; - return this; - } - - /** - * Sets the hash to be signed in the signature protocol. - *

- * The provided {@link SignableHash} must contain a valid hash value and hash type, - * which will be used as the digest in the signing request. - *

- * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. - * - * @param signableHash the hash data to be signed - * @return this builder - * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableData} - */ - public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { - if (this.digestInput != null && this.digestInput instanceof SignableData) { - throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData"); - } - this.digestInput = signableHash; - return this; - } - - /** - * Sends the signature request and initiates a notification-based signature session. - *

- * There are two supported ways to start the signature session: - *

    - *
  • with a document number by using {@link #withDocumentNumber(String)}
  • - *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • - *
- * - * @return a {@link NotificationSignatureSessionResponse} containing session details such as session ID and verification code - * @throws SmartIdRequestSetupException when the request parameters are not set correctly - * @throws UnprocessableSmartIdResponseException when the response from the Smart-ID service is invalid - */ - public NotificationSignatureSessionResponse initSignatureSession() { - validateRequestParameters(); - NotificationSignatureSessionRequest request = createSignatureSessionRequest(); - NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(request); - validateResponseParameters(notificationSignatureSessionResponse); - return notificationSignatureSessionResponse; - } - - private NotificationSignatureSessionResponse initSignatureSession(NotificationSignatureSessionRequest request) { - if (semanticsIdentifier != null && documentNumber != null) { - throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); - } - if (documentNumber != null) { - return connector.initNotificationSignature(request, documentNumber); - } else if (semanticsIdentifier != null) { - return connector.initNotificationSignature(request, semanticsIdentifier); - } else { - throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); - } - } - - private NotificationSignatureSessionRequest createSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), - signatureAlgorithm.getAlgorithmName(), - new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); - - return new NotificationSignatureSessionRequest(relyingPartyUUID, - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - nonce, - capabilities, - InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), - this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null - ); - } - - private void validateRequestParameters() { - if (StringUtil.isEmpty(relyingPartyUUID)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); - } - if (StringUtil.isEmpty(relyingPartyName)) { - throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); - } - if (signatureAlgorithm == null) { - throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); - } - if (digestInput == null) { - throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); - } - validateInteractions(); - if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { - throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); - } - } - - private void validateInteractions() { - if (InteractionUtil.isEmpty(interactions)) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); - } - if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { - throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); - } - } - - private void validateResponseParameters(NotificationSignatureSessionResponse response) { - if (StringUtil.isEmpty(response.sessionID())) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'sessionID' is missing or empty"); - } - - VerificationCode verificationCode = response.vc(); - if (verificationCode == null) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc' is missing"); - } - String vcType = verificationCode.type(); - if (StringUtil.isEmpty(vcType)) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' is missing or empty"); - } - if (!VerificationCodeType.NUMERIC4.getValue().equals(vcType)) { - logger.error("Notification-based signature response field 'vc.type' contains unsupported value '{}'", vcType); - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' contains unsupported value"); - } - if (StringUtil.isEmpty(verificationCode.value())) { - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' is missing or empty"); - } - if (!VERIFICATION_CODE_PATTERN.matcher(verificationCode.value()).matches()) { - logger.error("Notification-based signature response field 'vc.value' does not match the required pattern. Expected pattern: {}; actual value: {}", - VERIFICATION_CODE_PATTERN.pattern(), verificationCode.value()); - throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' does not match the required pattern"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.common.InteractionsMapper; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.InteractionUtil; +import ee.sk.smartid.util.SetUtil; +import ee.sk.smartid.util.StringUtil; + +/** + * Builder for creating a notification-based signature session + */ +public class NotificationSignatureSessionRequestBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationSignatureSessionRequestBuilder.class); + + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[0-9]{4}$"); + + private final SmartIdConnector connector; + + private String relyingPartyUUID; + private String relyingPartyName; + private String documentNumber; + private SemanticsIdentifier semanticsIdentifier; + private CertificateLevel certificateLevel; + private String nonce; + private Set capabilities; + private List interactions; + private Boolean shareMdClientIpAddress; + private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + private DigestInput digestInput; + + /** + * Constructs a new Smart-ID signature request builder with the given connector. + * + * @param connector the connector + */ + public NotificationSignatureSessionRequestBuilder(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Sets the relying party UUID. + * + * @param relyingPartyUUID the relying party UUID + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + return this; + } + + /** + * Sets the relying party name. + * + * @param relyingPartyName the relying party name + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + return this; + } + + /** + * Sets the document number. + * + * @param documentNumber the document number + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + return this; + } + + /** + * Sets the semantics identifier. + * + * @param semanticsIdentifier the semantics identifier + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSemanticsIdentifier(SemanticsIdentifier semanticsIdentifier) { + this.semanticsIdentifier = semanticsIdentifier; + return this; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + return this; + } + + /** + * Sets the nonce. + * + * @param nonce the nonce + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the capabilities. + * + * @param capabilities the capabilities + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withCapabilities(String... capabilities) { + this.capabilities = SetUtil.toSet(capabilities); + return this; + } + + /** + * Sets the interactions. + * + * @param interactions the allowed interactions order + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withInteractions(List interactions) { + this.interactions = interactions; + return this; + } + + /** + * Sets whether to share the Mobile device IP address + * + * @param shareMdClientIpAddress whether to share the Mobile device IP address + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withShareMdClientIpAddress(boolean shareMdClientIpAddress) { + this.shareMdClientIpAddress = shareMdClientIpAddress; + return this; + } + + /** + * Sets the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + * @return this builder + */ + public NotificationSignatureSessionRequestBuilder withSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + /** + * Sets the data to be signed. + *

+ * This method allows setting a {@link SignableData} object, which contains the data to be hashed and signed in the signing request. + *

+ * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. + * + * @param signableData the data to be signed + * @return this builder instance + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableHash} + */ + public NotificationSignatureSessionRequestBuilder withSignableData(SignableData signableData) { + if (this.digestInput != null && this.digestInput instanceof SignableHash) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableHash"); + } + this.digestInput = signableData; + return this; + } + + /** + * Sets the hash to be signed in the signature protocol. + *

+ * The provided {@link SignableHash} must contain a valid hash value and hash type, + * which will be used as the digest in the signing request. + *

+ * Only one of {@link #withSignableData(SignableData)} or {@link #withSignableHash(SignableHash)} may be used to set the digest input. + * + * @param signableHash the hash data to be signed + * @return this builder + * @throws SmartIdRequestSetupException if the digest input has already been set with {@link SignableData} + */ + public NotificationSignatureSessionRequestBuilder withSignableHash(SignableHash signableHash) { + if (this.digestInput != null && this.digestInput instanceof SignableData) { + throw new SmartIdRequestSetupException("Value for 'digestInput' has already been set with SignableData"); + } + this.digestInput = signableHash; + return this; + } + + /** + * Sends the signature request and initiates a notification-based signature session. + *

+ * There are two supported ways to start the signature session: + *

    + *
  • with a document number by using {@link #withDocumentNumber(String)}
  • + *
  • with a semantics identifier by using {@link #withSemanticsIdentifier(SemanticsIdentifier)}
  • + *
+ * + * @return a {@link NotificationSignatureSessionResponse} containing session details such as session ID and verification code + * @throws SmartIdRequestSetupException when the request parameters are not set correctly + * @throws UnprocessableSmartIdResponseException when the response from the Smart-ID service is invalid + */ + public NotificationSignatureSessionResponse initSignatureSession() { + validateRequestParameters(); + NotificationSignatureSessionRequest request = createSignatureSessionRequest(); + NotificationSignatureSessionResponse notificationSignatureSessionResponse = initSignatureSession(request); + validateResponseParameters(notificationSignatureSessionResponse); + return notificationSignatureSessionResponse; + } + + private NotificationSignatureSessionResponse initSignatureSession(NotificationSignatureSessionRequest request) { + if (semanticsIdentifier != null && documentNumber != null) { + throw new SmartIdRequestSetupException("Only one of 'semanticsIdentifier' or 'documentNumber' may be set"); + } + if (documentNumber != null) { + return connector.initNotificationSignature(request, documentNumber); + } else if (semanticsIdentifier != null) { + return connector.initNotificationSignature(request, semanticsIdentifier); + } else { + throw new SmartIdRequestSetupException("Either 'documentNumber' or 'semanticsIdentifier' must be set"); + } + } + + private NotificationSignatureSessionRequest createSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(digestInput.getDigestInBase64(), + signatureAlgorithm.getAlgorithmName(), + new SignatureAlgorithmParameters(digestInput.hashAlgorithm().getAlgorithmName())); + + return new NotificationSignatureSessionRequest(relyingPartyUUID, + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + nonce, + capabilities, + InteractionUtil.encodeToBase64(InteractionsMapper.from(interactions)), + this.shareMdClientIpAddress != null ? new RequestProperties(this.shareMdClientIpAddress) : null + ); + } + + private void validateRequestParameters() { + if (StringUtil.isEmpty(relyingPartyUUID)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyUUID' cannot be empty"); + } + if (StringUtil.isEmpty(relyingPartyName)) { + throw new SmartIdRequestSetupException("Value for 'relyingPartyName' cannot be empty"); + } + if (signatureAlgorithm == null) { + throw new SmartIdRequestSetupException("Value for 'signatureAlgorithm' must be set"); + } + if (digestInput == null) { + throw new SmartIdRequestSetupException("Value for 'digestInput' must be set with either SignableData or SignableHash"); + } + validateInteractions(); + if (nonce != null && (nonce.isEmpty() || nonce.length() > 30)) { + throw new SmartIdRequestSetupException("Value for 'nonce' length must be between 1 and 30 characters"); + } + } + + private void validateInteractions() { + if (InteractionUtil.isEmpty(interactions)) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot be empty"); + } + if (interactions.stream().map(NotificationInteraction::type).distinct().count() != interactions.size()) { + throw new SmartIdRequestSetupException("Value for 'interactions' cannot contain duplicate types"); + } + } + + private void validateResponseParameters(NotificationSignatureSessionResponse response) { + if (StringUtil.isEmpty(response.sessionID())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'sessionID' is missing or empty"); + } + + VerificationCode verificationCode = response.vc(); + if (verificationCode == null) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc' is missing"); + } + String vcType = verificationCode.type(); + if (StringUtil.isEmpty(vcType)) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' is missing or empty"); + } + if (!VerificationCodeType.NUMERIC4.getValue().equals(vcType)) { + logger.error("Notification-based signature response field 'vc.type' contains unsupported value '{}'", vcType); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.type' contains unsupported value"); + } + if (StringUtil.isEmpty(verificationCode.value())) { + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' is missing or empty"); + } + if (!VERIFICATION_CODE_PATTERN.matcher(verificationCode.value()).matches()) { + logger.error("Notification-based signature response field 'vc.value' does not match the required pattern. Expected pattern: {}; actual value: {}", + VERIFICATION_CODE_PATTERN.pattern(), verificationCode.value()); + throw new UnprocessableSmartIdResponseException("Notification-based signature response field 'vc.value' does not match the required pattern"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/QrCodeGenerator.java b/src/main/java/ee/sk/smartid/QrCodeGenerator.java index 7e3a3844..4daed4e7 100644 --- a/src/main/java/ee/sk/smartid/QrCodeGenerator.java +++ b/src/main/java/ee/sk/smartid/QrCodeGenerator.java @@ -1,142 +1,142 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; -import static com.google.zxing.EncodeHintType.MARGIN; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; - -import javax.imageio.ImageIO; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * This class is responsible for generating QR-codes. - * It can generate QR-codes as Data URIs or as BufferedImages. - *

- * The default image size of the generated QR code is 610x610px. - * It is calculated based on the version 9 QR-code that contains 53x53 modules and four quiet area modules. - * QR-code version 9 is selected automatically based on the provided length of the data. - * The module size should be 10px, so the image size is 53x10=530px + 2x4x10=80px = 610px. - *

- * Generated QR-codes have LOW error correction level. - */ -public class QrCodeGenerator { - - private static final int DEFAULT_QR_CODE_WIDTH_PX = 610; - private static final int DEFAULT_QR_CODE_HEIGHT = 610; - private static final int DEFAULT_QUIET_AREA_SIZE_MODULES = 4; - private static final String DEFAULT_FILE_FORMAT = "png"; - - /** - * Generates a QR-code as Data URI - *

- * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). - * - * @param data the data to be encoded - * @return the QR-code as a Base64 encoded string - */ - public static String generateDataUri(String data) { - BufferedImage bufferedImage = generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); - return convertToDataUri(bufferedImage, DEFAULT_FILE_FORMAT); - } - - /** - * Generates a QR-code as BufferedImage - *

- * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). - * - * @param data the data to be encoded - * @return the QR-code as a BufferedImage - */ - public static BufferedImage generateImage(String data) { - return generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); - } - - /** - * Generates a QR-code as BufferedImage. - *

- * Provide the width and height of the image in pixels and the size of the quiet area around the QR-code in modules. - * - * @param data the data to be encoded - * @param widthPx the width of the image in pixels - * @param heightPx the height of the image in pixels - * @param quietAreaSize the size of the quiet area around the QR-code, value in modules - * @return the QR-code as a BufferedImage - */ - public static BufferedImage generateImage(String data, int widthPx, int heightPx, int quietAreaSize) { - if (data == null || data.isEmpty()) { - throw new SmartIdClientException("Provided data cannot be empty"); - } - BitMatrix matrix; - try { - Map hints = new HashMap<>(); - hints.put(MARGIN, quietAreaSize); - hints.put(ERROR_CORRECTION, ErrorCorrectionLevel.L); - - matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, widthPx, heightPx, hints); - } catch (WriterException ex) { - throw new SmartIdClientException("Unable to create QR-code", ex); - } - return MatrixToImageWriter.toBufferedImage(matrix); - } - - /** - * Converts provided BufferedImage to Data URI with provided file format. - * - * @param bufferedImage the image to be converted - * @param fileFormat the format of the image - * @return the image as a Data URI - */ - public static String convertToDataUri(BufferedImage bufferedImage, String fileFormat) { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ImageIO.write(bufferedImage, fileFormat, outputStream); - String imgBase64 = Base64.getMimeEncoder().encodeToString(outputStream.toByteArray()); - return toDataUri(imgBase64, fileFormat); - } catch (IOException ex) { - throw new SmartIdClientException("Unable to generate QR-code", ex); - } - } - - private static String toDataUri(String imageData, String fileFormat) { - return String.format("data:image/%s;base64,%s", fileFormat, imageData); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; +import static com.google.zxing.EncodeHintType.MARGIN; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * This class is responsible for generating QR-codes. + * It can generate QR-codes as Data URIs or as BufferedImages. + *

+ * The default image size of the generated QR code is 610x610px. + * It is calculated based on the version 9 QR-code that contains 53x53 modules and four quiet area modules. + * QR-code version 9 is selected automatically based on the provided length of the data. + * The module size should be 10px, so the image size is 53x10=530px + 2x4x10=80px = 610px. + *

+ * Generated QR-codes have LOW error correction level. + */ +public class QrCodeGenerator { + + private static final int DEFAULT_QR_CODE_WIDTH_PX = 610; + private static final int DEFAULT_QR_CODE_HEIGHT = 610; + private static final int DEFAULT_QUIET_AREA_SIZE_MODULES = 4; + private static final String DEFAULT_FILE_FORMAT = "png"; + + /** + * Generates a QR-code as Data URI + *

+ * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a Base64 encoded string + */ + public static String generateDataUri(String data) { + BufferedImage bufferedImage = generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + return convertToDataUri(bufferedImage, DEFAULT_FILE_FORMAT); + } + + /** + * Generates a QR-code as BufferedImage + *

+ * Uses default values for width (610px), height (610px), quiet area (4 modules) and file type (PNG). + * + * @param data the data to be encoded + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data) { + return generateImage(data, DEFAULT_QR_CODE_WIDTH_PX, DEFAULT_QR_CODE_HEIGHT, DEFAULT_QUIET_AREA_SIZE_MODULES); + } + + /** + * Generates a QR-code as BufferedImage. + *

+ * Provide the width and height of the image in pixels and the size of the quiet area around the QR-code in modules. + * + * @param data the data to be encoded + * @param widthPx the width of the image in pixels + * @param heightPx the height of the image in pixels + * @param quietAreaSize the size of the quiet area around the QR-code, value in modules + * @return the QR-code as a BufferedImage + */ + public static BufferedImage generateImage(String data, int widthPx, int heightPx, int quietAreaSize) { + if (data == null || data.isEmpty()) { + throw new SmartIdClientException("Provided data cannot be empty"); + } + BitMatrix matrix; + try { + Map hints = new HashMap<>(); + hints.put(MARGIN, quietAreaSize); + hints.put(ERROR_CORRECTION, ErrorCorrectionLevel.L); + + matrix = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, widthPx, heightPx, hints); + } catch (WriterException ex) { + throw new SmartIdClientException("Unable to create QR-code", ex); + } + return MatrixToImageWriter.toBufferedImage(matrix); + } + + /** + * Converts provided BufferedImage to Data URI with provided file format. + * + * @param bufferedImage the image to be converted + * @param fileFormat the format of the image + * @return the image as a Data URI + */ + public static String convertToDataUri(BufferedImage bufferedImage, String fileFormat) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, fileFormat, outputStream); + String imgBase64 = Base64.getMimeEncoder().encodeToString(outputStream.toByteArray()); + return toDataUri(imgBase64, fileFormat); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to generate QR-code", ex); + } + } + + private static String toDataUri(String imageData, String fileFormat) { + return String.format("data:image/%s;base64,%s", fileFormat, imageData); + } +} diff --git a/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java index 48bbd710..f0489e4b 100644 --- a/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidator.java @@ -1,128 +1,128 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the signature certificate is a qualified Smart-ID certificate and can be used for digital signing. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Qualified profile - *

- * Additionally, it will check that certificate can be used for qualified electronic signature by checking - * presence of QCStatements extension and that it contains the electronic signature OID. - */ -public class QualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { - - private static final Logger logger = LoggerFactory.getLogger(QualifiedSignatureCertificatePurposeValidator.class); - - private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); - - @Override - public void validate(X509Certificate certificate) { - validateCertificateHasQualifiedSmartIdCertificatePolicies(certificate); - validateCertificateCanBeUsedForSigning(certificate); - validateCertificateCanBeUsedForQualifiedElectronicSignature(certificate); - } - - private static void validateCertificateHasQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); - } - if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate does not contain required qualified certificate policy OIDs"); - } - } - - private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { - if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); - } - } - - private static void validateCertificateCanBeUsedForQualifiedElectronicSignature(X509Certificate certificate) { - byte[] extensionValue = certificate.getExtensionValue(Extension.qCStatements.getId()); - if (extensionValue == null) { - throw new UnprocessableSmartIdResponseException("Certificate does not have 'QCStatements' extension"); - } - if (!hasElectronicSigningOid(extensionValue)) { - throw new UnprocessableSmartIdResponseException("Certificate does not have electronic signature OID (" + ETSIQCObjectIdentifiers.id_etsi_qct_esign.getId() + ") in QCStatements extension."); - } - } - - private static boolean hasElectronicSigningOid(byte[] extensionValue) { - ASN1Primitive prim; - try { - prim = ASN1Primitive.fromByteArray(ASN1OctetString.getInstance(extensionValue).getOctets()); - } catch (IOException ex) { - throw new SmartIdClientException("Unable to parse QCStatements extension", ex); - } - - ASN1Sequence qcStatements = ASN1Sequence.getInstance(prim); - for (int i = 0; i < qcStatements.size(); i++) { - QCStatement qs = QCStatement.getInstance(qcStatements.getObjectAt(i)); - ASN1ObjectIdentifier stmtId = qs.getStatementId(); - - if (ETSIQCObjectIdentifiers.id_etsi_qcs_QcType.equals(stmtId)) { - ASN1Sequence typeSeq = ASN1Sequence.getInstance(qs.getStatementInfo()); - if (typeSeq == null) { - return false; - } - for (int j = 0; j < typeSeq.size(); j++) { - ASN1ObjectIdentifier typeOid = ASN1ObjectIdentifier.getInstance(typeSeq.getObjectAt(j)); - if (ETSIQCObjectIdentifiers.id_etsi_qct_esign.equals(typeOid)) { - return true; - } - } - } - } - return false; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the signature certificate is a qualified Smart-ID certificate and can be used for digital signing. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Qualified profile + *

+ * Additionally, it will check that certificate can be used for qualified electronic signature by checking + * presence of QCStatements extension and that it contains the electronic signature OID. + */ +public class QualifiedSignatureCertificatePurposeValidator implements SignatureCertificatePurposeValidator { + + private static final Logger logger = LoggerFactory.getLogger(QualifiedSignatureCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.194112.1.2"); + + @Override + public void validate(X509Certificate certificate) { + validateCertificateHasQualifiedSmartIdCertificatePolicies(certificate); + validateCertificateCanBeUsedForSigning(certificate); + validateCertificateCanBeUsedForQualifiedElectronicSignature(certificate); + } + + private static void validateCertificateHasQualifiedSmartIdCertificatePolicies(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate does not contain required qualified certificate policy OIDs"); + } + } + + private static void validateCertificateCanBeUsedForSigning(X509Certificate certificate) { + if (!CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have Non-Repudiation set in 'KeyUsage' extension"); + } + } + + private static void validateCertificateCanBeUsedForQualifiedElectronicSignature(X509Certificate certificate) { + byte[] extensionValue = certificate.getExtensionValue(Extension.qCStatements.getId()); + if (extensionValue == null) { + throw new UnprocessableSmartIdResponseException("Certificate does not have 'QCStatements' extension"); + } + if (!hasElectronicSigningOid(extensionValue)) { + throw new UnprocessableSmartIdResponseException("Certificate does not have electronic signature OID (" + ETSIQCObjectIdentifiers.id_etsi_qct_esign.getId() + ") in QCStatements extension."); + } + } + + private static boolean hasElectronicSigningOid(byte[] extensionValue) { + ASN1Primitive prim; + try { + prim = ASN1Primitive.fromByteArray(ASN1OctetString.getInstance(extensionValue).getOctets()); + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse QCStatements extension", ex); + } + + ASN1Sequence qcStatements = ASN1Sequence.getInstance(prim); + for (int i = 0; i < qcStatements.size(); i++) { + QCStatement qs = QCStatement.getInstance(qcStatements.getObjectAt(i)); + ASN1ObjectIdentifier stmtId = qs.getStatementId(); + + if (ETSIQCObjectIdentifiers.id_etsi_qcs_QcType.equals(stmtId)) { + ASN1Sequence typeSeq = ASN1Sequence.getInstance(qs.getStatementInfo()); + if (typeSeq == null) { + return false; + } + for (int j = 0; j < typeSeq.size(); j++) { + ASN1ObjectIdentifier typeOid = ASN1ObjectIdentifier.getInstance(typeSeq.getObjectAt(j)); + if (ETSIQCObjectIdentifiers.id_etsi_qct_esign.equals(typeOid)) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/ee/sk/smartid/RpChallenge.java b/src/main/java/ee/sk/smartid/RpChallenge.java index 0063dfa3..eefc7ac7 100644 --- a/src/main/java/ee/sk/smartid/RpChallenge.java +++ b/src/main/java/ee/sk/smartid/RpChallenge.java @@ -1,55 +1,55 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import org.bouncycastle.util.encoders.Base64; - -/** - * Represents an RP challenge - * - * @param value a byte array of representing the challenge - */ -public record RpChallenge(byte[] value) { - - /** - * Returns a copy of the challenge value - * - * @return a byte array representing the challenge - */ - public byte[] value() { - return value.clone(); - } - - /** - * Returns the Base64 encoded representation of the challenge value - * - * @return a Base64 encoded string representing the challenge - */ - public String toBase64EncodedValue() { - return Base64.toBase64String(value); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import org.bouncycastle.util.encoders.Base64; + +/** + * Represents an RP challenge + * + * @param value a byte array of representing the challenge + */ +public record RpChallenge(byte[] value) { + + /** + * Returns a copy of the challenge value + * + * @return a byte array representing the challenge + */ + public byte[] value() { + return value.clone(); + } + + /** + * Returns the Base64 encoded representation of the challenge value + * + * @return a Base64 encoded string representing the challenge + */ + public String toBase64EncodedValue() { + return Base64.toBase64String(value); + } +} diff --git a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java index a2a97f68..d55d62d9 100644 --- a/src/main/java/ee/sk/smartid/RpChallengeGenerator.java +++ b/src/main/java/ee/sk/smartid/RpChallengeGenerator.java @@ -1,74 +1,74 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.SecureRandom; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for generating RP challenge - */ -public class RpChallengeGenerator { - - private static final int MAX_LENGTH = 64; - private static final int MIN_LENGTH = 32; - - private RpChallengeGenerator() { - } - - /** - * Generates an RP challenge with a maximum length of 64 bytes - * - * @return RP challenge - */ - public static RpChallenge generate() { - byte[] randBytes = new byte[MAX_LENGTH]; - new SecureRandom().nextBytes(randBytes); - return new RpChallenge(randBytes); - } - - /** - * Generates an RP challenge with specified length - * - * @param length length of the challenge - * @return RP challenge - */ - public static RpChallenge generate(int length) { - if (length < MIN_LENGTH || length > MAX_LENGTH) { - throw new SmartIdClientException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); - } - byte[] randBytes = getRandomBytes(length); - return new RpChallenge(randBytes); - } - - private static byte[] getRandomBytes(int length) { - byte[] randBytes = new byte[length]; - new SecureRandom().nextBytes(randBytes); - return randBytes; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for generating RP challenge + */ +public class RpChallengeGenerator { + + private static final int MAX_LENGTH = 64; + private static final int MIN_LENGTH = 32; + + private RpChallengeGenerator() { + } + + /** + * Generates an RP challenge with a maximum length of 64 bytes + * + * @return RP challenge + */ + public static RpChallenge generate() { + byte[] randBytes = new byte[MAX_LENGTH]; + new SecureRandom().nextBytes(randBytes); + return new RpChallenge(randBytes); + } + + /** + * Generates an RP challenge with specified length + * + * @param length length of the challenge + * @return RP challenge + */ + public static RpChallenge generate(int length) { + if (length < MIN_LENGTH || length > MAX_LENGTH) { + throw new SmartIdClientException("Length must be between " + MIN_LENGTH + " and " + MAX_LENGTH); + } + byte[] randBytes = getRandomBytes(length); + return new RpChallenge(randBytes); + } + + private static byte[] getRandomBytes(int length) { + byte[] randBytes = new byte[length]; + new SecureRandom().nextBytes(randBytes); + return randBytes; + } +} diff --git a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java index db7c0671..81bc797f 100644 --- a/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java +++ b/src/main/java/ee/sk/smartid/RsaSsaPssParameters.java @@ -1,140 +1,140 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Encapsulates multiple parameters of RSASSA-PSS - */ -public class RsaSsaPssParameters { - - private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; - - private HashAlgorithm digestHashAlgorithm; - private MaskGenAlgorithm maskGenAlgorithm; - private HashAlgorithm maskHashAlgorithm; - private int saltLength; - private TrailerField trailerField; - - /** - * Sets the hash algorithm - * - * @param digestHashAlgorithm the hash algorithm; see {@link HashAlgorithm} - */ - public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { - this.digestHashAlgorithm = digestHashAlgorithm; - } - - /** - * Sets the mask generation algorithm - * - * @param maskGenAlgorithm the mask generation algorithm; see {@link MaskGenAlgorithm} - */ - public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - /** - * Sets the mask hash algorithm - * - * @param maskHashAlgorithm the mask hash algorithm; see {@link HashAlgorithm} - */ - public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { - this.maskHashAlgorithm = maskHashAlgorithm; - } - - /** - * Sets the salt length - * - * @param saltLength the salt length in bytes - */ - public void setSaltLength(int saltLength) { - this.saltLength = saltLength; - } - - /** - * Sets the trailer field - * - * @param trailerField the trailer field; see {@link TrailerField} - */ - public void setTrailerField(TrailerField trailerField) { - this.trailerField = trailerField; - } - - /** - * Gets the signature algorithm - * - * @return the signature algorithm; see {@link SignatureAlgorithm} - */ - public SignatureAlgorithm getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Gets the hash algorithm - * - * @return the hash algorithm; see {@link HashAlgorithm} - */ - public HashAlgorithm getDigestHashAlgorithm() { - return digestHashAlgorithm; - } - - /** - * Gets the mask generation algorithm - * - * @return the mask generation algorithm - */ - public MaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - /** - * Gets the mask hash algorithm - * - * @return the mask hash algorithm - */ - public HashAlgorithm getMaskHashAlgorithm() { - return maskHashAlgorithm; - } - - /** - * Gets the salt length - * - * @return the salt length in bytes - */ - public int getSaltLength() { - return saltLength; - } - - /** - * Gets the trailer field - * - * @return the trailer field - */ - public TrailerField getTrailerField() { - return trailerField; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Encapsulates multiple parameters of RSASSA-PSS + */ +public class RsaSsaPssParameters { + + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSASSA_PSS; + + private HashAlgorithm digestHashAlgorithm; + private MaskGenAlgorithm maskGenAlgorithm; + private HashAlgorithm maskHashAlgorithm; + private int saltLength; + private TrailerField trailerField; + + /** + * Sets the hash algorithm + * + * @param digestHashAlgorithm the hash algorithm; see {@link HashAlgorithm} + */ + public void setDigestHashAlgorithm(HashAlgorithm digestHashAlgorithm) { + this.digestHashAlgorithm = digestHashAlgorithm; + } + + /** + * Sets the mask generation algorithm + * + * @param maskGenAlgorithm the mask generation algorithm; see {@link MaskGenAlgorithm} + */ + public void setMaskGenAlgorithm(MaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + /** + * Sets the mask hash algorithm + * + * @param maskHashAlgorithm the mask hash algorithm; see {@link HashAlgorithm} + */ + public void setMaskHashAlgorithm(HashAlgorithm maskHashAlgorithm) { + this.maskHashAlgorithm = maskHashAlgorithm; + } + + /** + * Sets the salt length + * + * @param saltLength the salt length in bytes + */ + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + /** + * Sets the trailer field + * + * @param trailerField the trailer field; see {@link TrailerField} + */ + public void setTrailerField(TrailerField trailerField) { + this.trailerField = trailerField; + } + + /** + * Gets the signature algorithm + * + * @return the signature algorithm; see {@link SignatureAlgorithm} + */ + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Gets the hash algorithm + * + * @return the hash algorithm; see {@link HashAlgorithm} + */ + public HashAlgorithm getDigestHashAlgorithm() { + return digestHashAlgorithm; + } + + /** + * Gets the mask generation algorithm + * + * @return the mask generation algorithm + */ + public MaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + /** + * Gets the mask hash algorithm + * + * @return the mask hash algorithm + */ + public HashAlgorithm getMaskHashAlgorithm() { + return maskHashAlgorithm; + } + + /** + * Gets the salt length + * + * @return the salt length in bytes + */ + public int getSaltLength() { + return saltLength; + } + + /** + * Gets the trailer field + * + * @return the trailer field + */ + public TrailerField getTrailerField() { + return trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/SessionType.java b/src/main/java/ee/sk/smartid/SessionType.java index 7a8c58f7..be0b8b0f 100644 --- a/src/main/java/ee/sk/smartid/SessionType.java +++ b/src/main/java/ee/sk/smartid/SessionType.java @@ -1,61 +1,61 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Represents session types used to construct different device links for Smart-ID sessions. - */ -public enum SessionType { - - /** - * Authentication session type - */ - AUTHENTICATION("auth"), - /** - * Signature session type - */ - SIGNATURE("sign"), - /** - * Certificate choice session type - */ - CERTIFICATE_CHOICE("cert"); - - private final String value; - - SessionType(String value) { - this.value = value; - } - - /** - * Returns the value used in the device link for the session type. - * - * @return the string value of the session type. - */ - public String getValue() { - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Represents session types used to construct different device links for Smart-ID sessions. + */ +public enum SessionType { + + /** + * Authentication session type + */ + AUTHENTICATION("auth"), + /** + * Signature session type + */ + SIGNATURE("sign"), + /** + * Certificate choice session type + */ + CERTIFICATE_CHOICE("cert"); + + private final String value; + + SessionType(String value) { + this.value = value; + } + + /** + * Returns the value used in the device link for the session type. + * + * @return the string value of the session type. + */ + public String getValue() { + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/SignableData.java b/src/main/java/ee/sk/smartid/SignableData.java index b40af066..29a7495a 100644 --- a/src/main/java/ee/sk/smartid/SignableData.java +++ b/src/main/java/ee/sk/smartid/SignableData.java @@ -1,93 +1,93 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * This class can be used to contain the data - * to be signed when it is not yet in hashed format - *

- * {@link SignableHash} can be used - * instead when the data to be signed is already - * in hashed format. - */ -public record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - - /** - * Creates a new instance of SignableData - *

- * Will use SHA-512 as the default hashing algorithm - * - * @param dataToSign byte array of data to be signed - */ - public SignableData(byte[] dataToSign) { - this(dataToSign, HashAlgorithm.SHA_512); - } - - /** - * Creates a new instance of SignableData - * - * @param dataToSign byte array of data to be signed - * @param hashAlgorithm hashing algorithm to be used - * @throws SmartIdRequestSetupException when input values are missing or empty - */ - public SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) { - if (dataToSign == null || dataToSign.length == 0) { - throw new SmartIdRequestSetupException("Parameter 'dataToSign' cannot be empty"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); - } - this.dataToSign = dataToSign.clone(); - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Calculates the digest of the data to be signed - * and returns it in Base64 encoded format - * - * @return Base64 encoded hash - */ - @Override - public String getDigestInBase64() { - byte[] digest = calculateHash(); - return Base64.getEncoder().encodeToString(digest); - } - - /** - * Calculates the digest of the data to be signed - * - * @return hash - */ - public byte[] calculateHash() { - return DigestCalculator.calculateDigest(dataToSign, hashAlgorithm); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * This class can be used to contain the data + * to be signed when it is not yet in hashed format + *

+ * {@link SignableHash} can be used + * instead when the data to be signed is already + * in hashed format. + */ +public record SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { + + /** + * Creates a new instance of SignableData + *

+ * Will use SHA-512 as the default hashing algorithm + * + * @param dataToSign byte array of data to be signed + */ + public SignableData(byte[] dataToSign) { + this(dataToSign, HashAlgorithm.SHA_512); + } + + /** + * Creates a new instance of SignableData + * + * @param dataToSign byte array of data to be signed + * @param hashAlgorithm hashing algorithm to be used + * @throws SmartIdRequestSetupException when input values are missing or empty + */ + public SignableData(byte[] dataToSign, HashAlgorithm hashAlgorithm) { + if (dataToSign == null || dataToSign.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'dataToSign' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } + this.dataToSign = dataToSign.clone(); + this.hashAlgorithm = hashAlgorithm; + } + + /** + * Calculates the digest of the data to be signed + * and returns it in Base64 encoded format + * + * @return Base64 encoded hash + */ + @Override + public String getDigestInBase64() { + byte[] digest = calculateHash(); + return Base64.getEncoder().encodeToString(digest); + } + + /** + * Calculates the digest of the data to be signed + * + * @return hash + */ + public byte[] calculateHash() { + return DigestCalculator.calculateDigest(dataToSign, hashAlgorithm); + } +} diff --git a/src/main/java/ee/sk/smartid/SignableHash.java b/src/main/java/ee/sk/smartid/SignableHash.java index 8bc47191..9d18117a 100644 --- a/src/main/java/ee/sk/smartid/SignableHash.java +++ b/src/main/java/ee/sk/smartid/SignableHash.java @@ -1,87 +1,87 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * This class can be used to contain the hash - * to be signed - *

- * {@link SignableData} can be used - * instead when the data to be signed is not already - * in hashed format. - */ -public record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { - - /** - * Creates {@link SignableHash} instance, - *

- * Will use SHA-512 as the default hashing algorithm - * - * @param hashToSign byte array of hash to be signed - * @throws SmartIdRequestSetupException when hashToSign is missing or empty - */ - public SignableHash(byte[] hashToSign) { - this(hashToSign, HashAlgorithm.SHA_512); - } - - /** - * Creates {@link SignableHash} instance - * - * @param hashToBeSigned byte array of hash to be signed - * @param hashAlgorithm hashing algorithm used to create the hash - * @throws SmartIdRequestSetupException when input parameters are missing or empty - */ - public SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) { - validateInputs(hashToBeSigned, hashAlgorithm); - this.hashToBeSigned = hashToBeSigned.clone(); - this.hashAlgorithm = hashAlgorithm; - } - - private static void validateInputs(byte[] hash, HashAlgorithm hashAlgorithm) { - if (hash == null || hash.length == 0) { - throw new SmartIdRequestSetupException("Parameter 'hash' cannot be empty"); - } - if (hashAlgorithm == null) { - throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); - } - } - - /** - * Get the hash as Base64-encoded string - * - * @return String - */ - @Override - public String getDigestInBase64() { - return Base64.getEncoder().encodeToString(hashToBeSigned); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * This class can be used to contain the hash + * to be signed + *

+ * {@link SignableData} can be used + * instead when the data to be signed is not already + * in hashed format. + */ +public record SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) implements Serializable, DigestInput { + + /** + * Creates {@link SignableHash} instance, + *

+ * Will use SHA-512 as the default hashing algorithm + * + * @param hashToSign byte array of hash to be signed + * @throws SmartIdRequestSetupException when hashToSign is missing or empty + */ + public SignableHash(byte[] hashToSign) { + this(hashToSign, HashAlgorithm.SHA_512); + } + + /** + * Creates {@link SignableHash} instance + * + * @param hashToBeSigned byte array of hash to be signed + * @param hashAlgorithm hashing algorithm used to create the hash + * @throws SmartIdRequestSetupException when input parameters are missing or empty + */ + public SignableHash(byte[] hashToBeSigned, HashAlgorithm hashAlgorithm) { + validateInputs(hashToBeSigned, hashAlgorithm); + this.hashToBeSigned = hashToBeSigned.clone(); + this.hashAlgorithm = hashAlgorithm; + } + + private static void validateInputs(byte[] hash, HashAlgorithm hashAlgorithm) { + if (hash == null || hash.length == 0) { + throw new SmartIdRequestSetupException("Parameter 'hash' cannot be empty"); + } + if (hashAlgorithm == null) { + throw new SmartIdRequestSetupException("Parameter 'hashAlgorithm' must be set"); + } + } + + /** + * Get the hash as Base64-encoded string + * + * @return String + */ + @Override + public String getDigestInBase64() { + return Base64.getEncoder().encodeToString(hashToBeSigned); + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java index 2ae5afb9..f6a33872 100644 --- a/src/main/java/ee/sk/smartid/SignatureAlgorithm.java +++ b/src/main/java/ee/sk/smartid/SignatureAlgorithm.java @@ -1,82 +1,82 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * Signature algorithms supported by Smart-ID API. - */ -public enum SignatureAlgorithm { - - /** - * RSASSA-PSS (RSA Probabilistic Signature Scheme) as defined in PKCS #1 v2.1. - * This algorithm provides probabilistic signature generation for enhanced security. - */ - RSASSA_PSS("rsassa-pss"); - - private final String algorithmName; - - SignatureAlgorithm(String algorithmName) { - this.algorithmName = algorithmName; - } - - /** - * Provides the signature algorithm name as used in the Smart-ID API. - * - * @return the signature algorithm name - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Checks if the provided signature algorithm is supported. - * - * @param signatureAlgorithm the signature algorithm name to check - * @return true if the signature algorithm is supported, false otherwise - */ - public static boolean isSupported(String signatureAlgorithm) { - return Arrays.stream(SignatureAlgorithm.values()) - .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); - } - - /** - * Converts a string representation of a signature algorithm to its corresponding enum value. - * - * @param signatureAlgorithm the signature algorithm name - * @return the corresponding SignatureAlgorithm enum value - * @throws IllegalArgumentException if the provided signature algorithm is not supported - */ - public static SignatureAlgorithm fromString(String signatureAlgorithm) { - return Arrays - .stream(SignatureAlgorithm.values()) - .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid signatureAlgorithm value: " + signatureAlgorithm)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * Signature algorithms supported by Smart-ID API. + */ +public enum SignatureAlgorithm { + + /** + * RSASSA-PSS (RSA Probabilistic Signature Scheme) as defined in PKCS #1 v2.1. + * This algorithm provides probabilistic signature generation for enhanced security. + */ + RSASSA_PSS("rsassa-pss"); + + private final String algorithmName; + + SignatureAlgorithm(String algorithmName) { + this.algorithmName = algorithmName; + } + + /** + * Provides the signature algorithm name as used in the Smart-ID API. + * + * @return the signature algorithm name + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Checks if the provided signature algorithm is supported. + * + * @param signatureAlgorithm the signature algorithm name to check + * @return true if the signature algorithm is supported, false otherwise + */ + public static boolean isSupported(String signatureAlgorithm) { + return Arrays.stream(SignatureAlgorithm.values()) + .anyMatch(s -> s.getAlgorithmName().equals(signatureAlgorithm)); + } + + /** + * Converts a string representation of a signature algorithm to its corresponding enum value. + * + * @param signatureAlgorithm the signature algorithm name + * @return the corresponding SignatureAlgorithm enum value + * @throws IllegalArgumentException if the provided signature algorithm is not supported + */ + public static SignatureAlgorithm fromString(String signatureAlgorithm) { + return Arrays + .stream(SignatureAlgorithm.values()) + .filter(s -> s.getAlgorithmName().equals(signatureAlgorithm)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid signatureAlgorithm value: " + signatureAlgorithm)); + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java index 7c2dbb29..3c91ed2d 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidator.java @@ -1,46 +1,46 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating whether a given X509 certificate is suitable for digital signing purposes. - * Implementations should check certificate properties and throw an exception if the certificate is not valid for signing. - */ -public interface SignatureCertificatePurposeValidator { - - /** - * Validates that the provided certificate is suitable for digital signing - * - * @param certificate certificate to validate - * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for digital signing - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating whether a given X509 certificate is suitable for digital signing purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for signing. + */ +public interface SignatureCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for digital signing + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for digital signing + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java index c623f9aa..972c4704 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactory.java @@ -1,41 +1,41 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Factory interface to create instances of SignatureCertificatePurposeValidator based on the certificate level. - */ -public interface SignatureCertificatePurposeValidatorFactory { - - /** - * Creates SignatureCertificatePurposeValidator based on the provided certificate level. - * - * @param certificateLevel the certificate level to create the validator for - * @return SignatureCertificatePurposeValidator instance - */ - SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Factory interface to create instances of SignatureCertificatePurposeValidator based on the certificate level. + */ +public interface SignatureCertificatePurposeValidatorFactory { + + /** + * Creates SignatureCertificatePurposeValidator based on the provided certificate level. + * + * @param certificateLevel the certificate level to create the validator for + * @return SignatureCertificatePurposeValidator instance + */ + SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java index 1c18bd8a..1f07b2c3 100644 --- a/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureCertificatePurposeValidatorFactoryImpl.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Factory to create Qualified or Non-Qualified SignatureCertificatePurposeValidator based on the certificate level. - * Will be used to validate the certificate purpose of the signature certificate. - *

- * Only QUALIFIED and ADVANCED certificate levels are supported, - * because QUALIFIED level certificate will also be returned for QSCD. - */ -public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { - - @Override - public SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel) { - if (certificateLevel == null) { - throw new SmartIdClientException("Parameter 'certificateLevel' is not provided"); - } - return switch (certificateLevel) { - case QUALIFIED -> new QualifiedSignatureCertificatePurposeValidator(); - case ADVANCED -> new NonQualifiedSignatureCertificatePurposeValidator(); - default -> throw new SmartIdClientException("Unsupported certificate level: " + certificateLevel); - }; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Factory to create Qualified or Non-Qualified SignatureCertificatePurposeValidator based on the certificate level. + * Will be used to validate the certificate purpose of the signature certificate. + *

+ * Only QUALIFIED and ADVANCED certificate levels are supported, + * because QUALIFIED level certificate will also be returned for QSCD. + */ +public class SignatureCertificatePurposeValidatorFactoryImpl implements SignatureCertificatePurposeValidatorFactory { + + @Override + public SignatureCertificatePurposeValidator create(CertificateLevel certificateLevel) { + if (certificateLevel == null) { + throw new SmartIdClientException("Parameter 'certificateLevel' is not provided"); + } + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedSignatureCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedSignatureCertificatePurposeValidator(); + default -> throw new SmartIdClientException("Unsupported certificate level: " + certificateLevel); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureProtocol.java b/src/main/java/ee/sk/smartid/SignatureProtocol.java index d30f0db1..4ee32e67 100644 --- a/src/main/java/ee/sk/smartid/SignatureProtocol.java +++ b/src/main/java/ee/sk/smartid/SignatureProtocol.java @@ -1,43 +1,43 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Signature protocols supported by Smart-ID API. - */ -public enum SignatureProtocol { - - /** - * Signature protocol used for authentication. - */ - ACSP_V2, - - /** - * Signature protocol used for signature. - */ - RAW_DIGEST_SIGNATURE -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Signature protocols supported by Smart-ID API. + */ +public enum SignatureProtocol { + + /** + * Signature protocol used for authentication. + */ + ACSP_V2, + + /** + * Signature protocol used for signature. + */ + RAW_DIGEST_SIGNATURE +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponse.java b/src/main/java/ee/sk/smartid/SignatureResponse.java index caad1cad..31e933d5 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponse.java +++ b/src/main/java/ee/sk/smartid/SignatureResponse.java @@ -1,273 +1,273 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.security.cert.X509Certificate; -import java.util.Base64; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Response of a completed and validated signature session. - */ -public class SignatureResponse implements Serializable { - - private String endResult; - private String signatureValueInBase64; - private String algorithmName; - private SignatureAlgorithm signatureAlgorithm; - private FlowType flowType; - private X509Certificate certificate; - private CertificateLevel requestedCertificateLevel; - private CertificateLevel certificateLevel; - private String documentNumber; - private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name 'interactionTypeUsed'; Fix in SLIB-138 - private String deviceIpAddress; - private RsaSsaPssParameters rsaSsaPssParameters; - - /** - * Gets the signature value as a byte array by decoding the base64-encoded string. - * - * @return the signature value as a byte array - * @throws UnprocessableSmartIdResponseException if the base64 string is incorrectly encoded - */ - public byte[] getSignatureValue() { - try { - return Base64.getDecoder().decode(signatureValueInBase64); - } catch (IllegalArgumentException e) { - throw new UnprocessableSmartIdResponseException( - "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); - } - } - - /** - * Gets the end result of the signing operation. - *

- * returns the end result of the signing operation - */ - public String getEndResult() { - return endResult; - } - - /** - * Sets the end result of the signing operation. - * - * @param endResult the end result of the signing operation - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Gets the signature value as a base64-encoded string. - * - * @return the signature value in base64 - */ - public String getSignatureValueInBase64() { - return signatureValueInBase64; - } - - /** - * Sets the signature value as a base64-encoded string. - * - * @param signatureValueInBase64 the signature value in base64 - */ - public void setSignatureValueInBase64(String signatureValueInBase64) { - this.signatureValueInBase64 = signatureValueInBase64; - } - - /** - * Gets the name of the algorithm used for signing. - * - * @return the name of the algorithm - */ - public String getAlgorithmName() { - return algorithmName; - } - - /** - * Sets the name of the algorithm used for signing. - * - * @param algorithmName the name of the algorithm - */ - public void setAlgorithmName(String algorithmName) { - this.algorithmName = algorithmName; - } - - /** - * Gets the signature algorithm used for signing. - * - * @return the signature algorithm - */ - public SignatureAlgorithm getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Sets the signature algorithm used for signing. - * - * @param signatureAlgorithm the signature algorithm - */ - public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - /** - * Gets the flow type user used to complete the signing. - * - * @return the flow type - */ - public FlowType getFlowType() { - return flowType; - } - - /** - * Sets the flow type. - * - * @param flowType the flow type - */ - public void setFlowType(FlowType flowType) { - this.flowType = flowType; - } - - /** - * Gets the certificate used for signing. - * - * @return the X.509 certificate - */ - public X509Certificate getCertificate() { - return certificate; - } - - /** - * Sets the certificate used for signing. - * - * @param certificate the X.509 certificate - */ - public void setCertificate(X509Certificate certificate) { - this.certificate = certificate; - } - - /** - * Gets the certificate level of the certificate used for signing. - * - * @return the certificate level - */ - public CertificateLevel getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the certificate level of the certificate used for signing. - * - * @param certificateLevel the certificate level - */ - public void setCertificateLevel(CertificateLevel certificateLevel) { - this.certificateLevel = certificateLevel; - } - - /** - * Gets the requested certificate level for the signing operation. - * - * @return the requested certificate level - */ - public CertificateLevel getRequestedCertificateLevel() { - return requestedCertificateLevel; - } - - /** - * Sets the requested certificate level for the signing operation. - * - * @param requestedCertificateLevel the requested certificate level - */ - public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { - this.requestedCertificateLevel = requestedCertificateLevel; - } - - /** - * Gets the document number of the user who performed the signing. - * - * @return the document number - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Sets the document number of the user who performed the signing. - * - * @param documentNumber the document number - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - public String getInteractionFlowUsed() { - return interactionFlowUsed; - } - - public void setInteractionFlowUsed(String interactionFlowUsed) { - this.interactionFlowUsed = interactionFlowUsed; - } - - /** - * Gets the IP address of the device used by the user to complete the signing. - * - * @return the device IP address - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device. - * - * @param deviceIpAddress the device IP address - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } - - /** - * Gets the RSASSA-PSS parameters used in the signing operation. - * - * @return the RSASSA-PSS parameters. - */ - public RsaSsaPssParameters getRsaSsaPssParameters() { - return rsaSsaPssParameters; - } - - /** - * Sets the RSASSA-PSS parameters used in the signing operation. - * - * @param rsaSsaPssParameters the RSASSA-PSS parameters. - */ - public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { - this.rsaSsaPssParameters = rsaSsaPssParameters; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Response of a completed and validated signature session. + */ +public class SignatureResponse implements Serializable { + + private String endResult; + private String signatureValueInBase64; + private String algorithmName; + private SignatureAlgorithm signatureAlgorithm; + private FlowType flowType; + private X509Certificate certificate; + private CertificateLevel requestedCertificateLevel; + private CertificateLevel certificateLevel; + private String documentNumber; + private String interactionFlowUsed; // TODO - 10.10.25: should be renamed to match new field name 'interactionTypeUsed'; Fix in SLIB-138 + private String deviceIpAddress; + private RsaSsaPssParameters rsaSsaPssParameters; + + /** + * Gets the signature value as a byte array by decoding the base64-encoded string. + * + * @return the signature value as a byte array + * @throws UnprocessableSmartIdResponseException if the base64 string is incorrectly encoded + */ + public byte[] getSignatureValue() { + try { + return Base64.getDecoder().decode(signatureValueInBase64); + } catch (IllegalArgumentException e) { + throw new UnprocessableSmartIdResponseException( + "Failed to parse signature value in base64. Incorrectly encoded base64 string: '" + signatureValueInBase64 + "'"); + } + } + + /** + * Gets the end result of the signing operation. + *

+ * returns the end result of the signing operation + */ + public String getEndResult() { + return endResult; + } + + /** + * Sets the end result of the signing operation. + * + * @param endResult the end result of the signing operation + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Gets the signature value as a base64-encoded string. + * + * @return the signature value in base64 + */ + public String getSignatureValueInBase64() { + return signatureValueInBase64; + } + + /** + * Sets the signature value as a base64-encoded string. + * + * @param signatureValueInBase64 the signature value in base64 + */ + public void setSignatureValueInBase64(String signatureValueInBase64) { + this.signatureValueInBase64 = signatureValueInBase64; + } + + /** + * Gets the name of the algorithm used for signing. + * + * @return the name of the algorithm + */ + public String getAlgorithmName() { + return algorithmName; + } + + /** + * Sets the name of the algorithm used for signing. + * + * @param algorithmName the name of the algorithm + */ + public void setAlgorithmName(String algorithmName) { + this.algorithmName = algorithmName; + } + + /** + * Gets the signature algorithm used for signing. + * + * @return the signature algorithm + */ + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Sets the signature algorithm used for signing. + * + * @param signatureAlgorithm the signature algorithm + */ + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Gets the flow type user used to complete the signing. + * + * @return the flow type + */ + public FlowType getFlowType() { + return flowType; + } + + /** + * Sets the flow type. + * + * @param flowType the flow type + */ + public void setFlowType(FlowType flowType) { + this.flowType = flowType; + } + + /** + * Gets the certificate used for signing. + * + * @return the X.509 certificate + */ + public X509Certificate getCertificate() { + return certificate; + } + + /** + * Sets the certificate used for signing. + * + * @param certificate the X.509 certificate + */ + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + /** + * Gets the certificate level of the certificate used for signing. + * + * @return the certificate level + */ + public CertificateLevel getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the certificate level of the certificate used for signing. + * + * @param certificateLevel the certificate level + */ + public void setCertificateLevel(CertificateLevel certificateLevel) { + this.certificateLevel = certificateLevel; + } + + /** + * Gets the requested certificate level for the signing operation. + * + * @return the requested certificate level + */ + public CertificateLevel getRequestedCertificateLevel() { + return requestedCertificateLevel; + } + + /** + * Sets the requested certificate level for the signing operation. + * + * @param requestedCertificateLevel the requested certificate level + */ + public void setRequestedCertificateLevel(CertificateLevel requestedCertificateLevel) { + this.requestedCertificateLevel = requestedCertificateLevel; + } + + /** + * Gets the document number of the user who performed the signing. + * + * @return the document number + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Sets the document number of the user who performed the signing. + * + * @param documentNumber the document number + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + public String getInteractionFlowUsed() { + return interactionFlowUsed; + } + + public void setInteractionFlowUsed(String interactionFlowUsed) { + this.interactionFlowUsed = interactionFlowUsed; + } + + /** + * Gets the IP address of the device used by the user to complete the signing. + * + * @return the device IP address + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device. + * + * @param deviceIpAddress the device IP address + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } + + /** + * Gets the RSASSA-PSS parameters used in the signing operation. + * + * @return the RSASSA-PSS parameters. + */ + public RsaSsaPssParameters getRsaSsaPssParameters() { + return rsaSsaPssParameters; + } + + /** + * Sets the RSASSA-PSS parameters used in the signing operation. + * + * @param rsaSsaPssParameters the RSASSA-PSS parameters. + */ + public void setRsaSsaPssParameters(RsaSsaPssParameters rsaSsaPssParameters) { + this.rsaSsaPssParameters = rsaSsaPssParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java index e2ad7e20..63b85de4 100644 --- a/src/main/java/ee/sk/smartid/SignatureResponseValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureResponseValidator.java @@ -1,340 +1,340 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.StringUtil; - -/** - * Validator for signature session status. - */ -public class SignatureResponseValidator { - - private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); - - private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); - - private final CertificateValidator certificateValidator; - private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; - - /** - * Initializes the validator with a {@link CertificateValidator} and a {@link SignatureCertificatePurposeValidatorFactory}. - * - * @param certificateValidator the certificate validator - * @param signatureCertificatePurposeValidatorFactory the signature certificate purpose validator factory - */ - public SignatureResponseValidator(CertificateValidator certificateValidator, - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { - this.certificateValidator = certificateValidator; - this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; - } - - /** - * Initializes the validator with a {@link CertificateValidator} - * - * @param certificateValidator the certificate validator - */ - public SignatureResponseValidator(CertificateValidator certificateValidator) { - this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); - } - - /** - * Validates {@link SessionStatus} and produces {@link SignatureResponse}. - * - * @param sessionStatus session status response - * @param requestedCertificateLevel certificate level used to start the signature session - * @return the signature response - * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. - * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame - * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code - * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. - * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. - * @throws SmartIdClientException if any of method parameters are not provided - */ - public SignatureResponse validate(SessionStatus sessionStatus, - CertificateLevel requestedCertificateLevel - ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { - validateSessionsStatus(sessionStatus, requestedCertificateLevel); - - SessionResult sessionResult = sessionStatus.getResult(); - SessionSignature sessionSignature = sessionStatus.getSignature(); - SessionCertificate certificate = sessionStatus.getCert(); - - var signatureResponse = new SignatureResponse(); - signatureResponse.setEndResult(sessionResult.getEndResult()); - signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); - signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); - - SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - var rsaSsaPssParams = new RsaSsaPssParameters(); - rsaSsaPssParams.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); - rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); - rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); - rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); - rsaSsaPssParams.setTrailerField(TrailerField.BC); - signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); - - signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); - signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); - signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); - signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); - signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); - signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); - signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); - - return signatureResponse; - } - - private void validateSessionsStatus(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - if (sessionStatus == null) { - throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); - } - - if (StringUtil.isEmpty(sessionStatus.getState())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'state' is empty"); - } - - if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); - } - - validateSessionResult(sessionStatus, requestedCertificateLevel); - } - - private void validateSessionResult(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { - SessionResult sessionResult = sessionStatus.getResult(); - - if (sessionResult == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result' is missing"); - } - - String endResult = sessionResult.getEndResult(); - if (StringUtil.isEmpty(endResult)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result.endResult' is empty"); - } - - if ("OK".equalsIgnoreCase(endResult)) { - if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'result.documentNumber' is empty"); - } - if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'interactionTypeUsed' is empty"); - } - if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' is empty"); - } - validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); - validateSignature(sessionStatus); - } else { - ErrorResultHandler.handle(sessionResult); - } - } - - private void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { - if (sessionCertificate == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing"); - } - if (StringUtil.isEmpty(sessionCertificate.getValue())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); - } - if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); - } - if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { - logger.error("Signature session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' has unsupported value"); - } - CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); - if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { - logger.error("Signature session status certificate level mismatch: requested {}, returned {}", - requestedCertificateLevel, sessionCertificate.getCertificateLevel()); - throw new CertificateLevelMismatchException(); - } - X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); - certificateValidator.validate(certificate); - - SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); - purposeValidator.validate(certificate); - } - - private static X509Certificate parseAndCheckCertificate(String certBase64) { - X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); - try { - certificate.checkValidity(); - } catch (CertificateExpiredException | CertificateNotYetValidException ex) { - logger.error("Signature certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); - throw new UnprocessableSmartIdResponseException("Signature certificate is invalid", ex); - } - return certificate; - } - - private static void validateSignature(SessionStatus sessionStatus) { - String signatureProtocol = sessionStatus.getSignatureProtocol(); - - if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { - validateRawDigestSignature(sessionStatus); - } else { - logger.error("Signature session status field 'signatureProtocol' has unsupported value: {}", signatureProtocol); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' has unsupported value"); - } - } - - private static void validateRawDigestSignature(SessionStatus sessionStatus) { - SessionSignature signature = sessionStatus.getSignature(); - if (signature == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature' is missing"); - } - - validateSignatureValue(signature.getValue()); - validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); - validateFlowType(signature.getFlowType()); - validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); - } - - private static void validateSignatureValue(String value) { - if (StringUtil.isEmpty(value)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' is empty"); - } - if (!BASE64_PATTERN.matcher(value).matches()) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' does not have Base64-encoded value"); - } - } - - private static void validateSignatureAlgorithmName(String signatureAlgorithm) { - if (StringUtil.isEmpty(signatureAlgorithm)) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' is missing"); - } - - if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { - List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); - logger.error("Signature session status field 'signature.signatureAlgorithm' has unsupported value: {}. Possible values: {}", signatureAlgorithm, possibleValues); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' has unsupported value"); - } - } - - private static void validateFlowType(String flowType) { - if (StringUtil.isEmpty(flowType)) { - throw new UnprocessableSmartIdResponseException("Signature session status field `signature.flowType` is empty"); - } - if (!FlowType.isSupported(flowType)) { - logger.error("Signature session status field `signature.flowType` has invalid value: {}", flowType); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.flowType' has unsupported value"); - } - } - - private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { - if (sessionSignatureAlgorithmParameters == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters' is missing"); - } - - if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); - } - - Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); - if (hashAlgorithm.isEmpty()) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); - } - - var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); - if (maskGenAlgorithm == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); - } - - if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); - } - - if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"); - } - - if (maskGenAlgorithm.getParameters() == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); - } - - if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); - } - - Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); - if (mgfHashAlgorithm.isEmpty()) { - logger.error("Signature session 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); - } - - if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", - hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); - } - - if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); - } - - int expectedSaltLength = hashAlgorithm.get().getOctetLength(); - int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); - if (expectedSaltLength != actualSaltLength) { - logger.error("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected {}, got {}", expectedSaltLength, actualSaltLength); - throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); - } - - if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { - throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); - } - - if (!TrailerField.BC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { - logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); - throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.StringUtil; + +/** + * Validator for signature session status. + */ +public class SignatureResponseValidator { + + private static final Logger logger = LoggerFactory.getLogger(SignatureResponseValidator.class); + + private static final Pattern BASE64_PATTERN = Pattern.compile("^[a-zA-Z0-9+/]+={0,2}$"); + + private final CertificateValidator certificateValidator; + private final SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory; + + /** + * Initializes the validator with a {@link CertificateValidator} and a {@link SignatureCertificatePurposeValidatorFactory}. + * + * @param certificateValidator the certificate validator + * @param signatureCertificatePurposeValidatorFactory the signature certificate purpose validator factory + */ + public SignatureResponseValidator(CertificateValidator certificateValidator, + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory) { + this.certificateValidator = certificateValidator; + this.signatureCertificatePurposeValidatorFactory = signatureCertificatePurposeValidatorFactory; + } + + /** + * Initializes the validator with a {@link CertificateValidator} + * + * @param certificateValidator the certificate validator + */ + public SignatureResponseValidator(CertificateValidator certificateValidator) { + this(certificateValidator, new SignatureCertificatePurposeValidatorFactoryImpl()); + } + + /** + * Validates {@link SessionStatus} and produces {@link SignatureResponse}. + * + * @param sessionStatus session status response + * @param requestedCertificateLevel certificate level used to start the signature session + * @return the signature response + * @throws UserRefusedException when the user has refused the session. NB! This exception has subclasses to determine the screen where user pressed cancel. + * @throws SessionTimeoutException when there was a timeout, i.e. end user did not confirm or refuse the operation within given time frame + * @throws UserSelectedWrongVerificationCodeException when user was presented with three control codes and user selected wrong code + * @throws DocumentUnusableException when for some reason, this relying party request cannot be completed. + * @throws UnprocessableSmartIdResponseException if the session response is structurally invalid, contains missing fields, or violates signature or certificate constraints. + * @throws SmartIdClientException if any of method parameters are not provided + */ + public SignatureResponse validate(SessionStatus sessionStatus, + CertificateLevel requestedCertificateLevel + ) throws UserRefusedException, UserSelectedWrongVerificationCodeException, SessionTimeoutException, DocumentUnusableException { + validateSessionsStatus(sessionStatus, requestedCertificateLevel); + + SessionResult sessionResult = sessionStatus.getResult(); + SessionSignature sessionSignature = sessionStatus.getSignature(); + SessionCertificate certificate = sessionStatus.getCert(); + + var signatureResponse = new SignatureResponse(); + signatureResponse.setEndResult(sessionResult.getEndResult()); + signatureResponse.setSignatureValueInBase64(sessionSignature.getValue()); + signatureResponse.setAlgorithmName(sessionSignature.getSignatureAlgorithm()); + + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + var rsaSsaPssParams = new RsaSsaPssParameters(); + rsaSsaPssParams.setDigestHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParams.setMaskHashAlgorithm(HashAlgorithm.fromString(signatureAlgorithmParameters.getMaskGenAlgorithm().getParameters().getHashAlgorithm()).orElse(null)); + rsaSsaPssParams.setSaltLength(signatureAlgorithmParameters.getSaltLength()); + rsaSsaPssParams.setTrailerField(TrailerField.BC); + signatureResponse.setRsaSsaPssParameters(rsaSsaPssParams); + + signatureResponse.setFlowType(FlowType.fromString(sessionSignature.getFlowType())); + signatureResponse.setCertificate(CertificateParser.parseX509Certificate(certificate.getValue())); + signatureResponse.setRequestedCertificateLevel(requestedCertificateLevel); + signatureResponse.setCertificateLevel(CertificateLevel.valueOf(certificate.getCertificateLevel())); + signatureResponse.setDocumentNumber(sessionResult.getDocumentNumber()); + signatureResponse.setInteractionFlowUsed(sessionStatus.getInteractionTypeUsed()); + signatureResponse.setDeviceIpAddress(sessionStatus.getDeviceIpAddress()); + + return signatureResponse; + } + + private void validateSessionsStatus(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + if (sessionStatus == null) { + throw new SmartIdClientException("Parameter 'sessionStatus' is not provided"); + } + + if (StringUtil.isEmpty(sessionStatus.getState())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'state' is empty"); + } + + if (!"COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + throw new SmartIdClientException("Session is not complete. State: " + sessionStatus.getState()); + } + + validateSessionResult(sessionStatus, requestedCertificateLevel); + } + + private void validateSessionResult(SessionStatus sessionStatus, CertificateLevel requestedCertificateLevel) { + SessionResult sessionResult = sessionStatus.getResult(); + + if (sessionResult == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result' is missing"); + } + + String endResult = sessionResult.getEndResult(); + if (StringUtil.isEmpty(endResult)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.endResult' is empty"); + } + + if ("OK".equalsIgnoreCase(endResult)) { + if (StringUtil.isEmpty(sessionResult.getDocumentNumber())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'result.documentNumber' is empty"); + } + if (StringUtil.isEmpty(sessionStatus.getInteractionTypeUsed())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'interactionTypeUsed' is empty"); + } + if (StringUtil.isEmpty(sessionStatus.getSignatureProtocol())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' is empty"); + } + validateCertificate(sessionStatus.getCert(), requestedCertificateLevel); + validateSignature(sessionStatus); + } else { + ErrorResultHandler.handle(sessionResult); + } + } + + private void validateCertificate(SessionCertificate sessionCertificate, CertificateLevel requestedCertificateLevel) { + if (sessionCertificate == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert' is missing"); + } + if (StringUtil.isEmpty(sessionCertificate.getValue())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.value' is empty"); + } + if (StringUtil.isEmpty(sessionCertificate.getCertificateLevel())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' is empty"); + } + if (!CertificateLevel.isSupported(sessionCertificate.getCertificateLevel())) { + logger.error("Signature session status field 'cert.certificateLevel' has invalid value: {}", sessionCertificate.getCertificateLevel()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'cert.certificateLevel' has unsupported value"); + } + CertificateLevel certificateLevel = CertificateLevel.valueOf(sessionCertificate.getCertificateLevel()); + if (!certificateLevel.isSameLevelOrHigher(requestedCertificateLevel)) { + logger.error("Signature session status certificate level mismatch: requested {}, returned {}", + requestedCertificateLevel, sessionCertificate.getCertificateLevel()); + throw new CertificateLevelMismatchException(); + } + X509Certificate certificate = parseAndCheckCertificate(sessionCertificate.getValue()); + certificateValidator.validate(certificate); + + SignatureCertificatePurposeValidator purposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateLevel); + purposeValidator.validate(certificate); + } + + private static X509Certificate parseAndCheckCertificate(String certBase64) { + X509Certificate certificate = CertificateParser.parseX509Certificate(certBase64); + try { + certificate.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException ex) { + logger.error("Signature certificate is expired or not yet valid: {}", certificate.getSubjectX500Principal(), ex); + throw new UnprocessableSmartIdResponseException("Signature certificate is invalid", ex); + } + return certificate; + } + + private static void validateSignature(SessionStatus sessionStatus) { + String signatureProtocol = sessionStatus.getSignatureProtocol(); + + if (SignatureProtocol.RAW_DIGEST_SIGNATURE.name().equalsIgnoreCase(signatureProtocol)) { + validateRawDigestSignature(sessionStatus); + } else { + logger.error("Signature session status field 'signatureProtocol' has unsupported value: {}", signatureProtocol); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signatureProtocol' has unsupported value"); + } + } + + private static void validateRawDigestSignature(SessionStatus sessionStatus) { + SessionSignature signature = sessionStatus.getSignature(); + if (signature == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature' is missing"); + } + + validateSignatureValue(signature.getValue()); + validateSignatureAlgorithmName(signature.getSignatureAlgorithm()); + validateFlowType(signature.getFlowType()); + validateSignatureAlgorithmParameters(signature.getSignatureAlgorithmParameters()); + } + + private static void validateSignatureValue(String value) { + if (StringUtil.isEmpty(value)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' is empty"); + } + if (!BASE64_PATTERN.matcher(value).matches()) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.value' does not have Base64-encoded value"); + } + } + + private static void validateSignatureAlgorithmName(String signatureAlgorithm) { + if (StringUtil.isEmpty(signatureAlgorithm)) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' is missing"); + } + + if (!SignatureAlgorithm.isSupported(signatureAlgorithm)) { + List possibleValues = Arrays.stream(SignatureAlgorithm.values()).map(SignatureAlgorithm::getAlgorithmName).toList(); + logger.error("Signature session status field 'signature.signatureAlgorithm' has unsupported value: {}. Possible values: {}", signatureAlgorithm, possibleValues); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithm' has unsupported value"); + } + } + + private static void validateFlowType(String flowType) { + if (StringUtil.isEmpty(flowType)) { + throw new UnprocessableSmartIdResponseException("Signature session status field `signature.flowType` is empty"); + } + if (!FlowType.isSupported(flowType)) { + logger.error("Signature session status field `signature.flowType` has invalid value: {}", flowType); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.flowType' has unsupported value"); + } + } + + private static void validateSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters sessionSignatureAlgorithmParameters) { + if (sessionSignatureAlgorithmParameters == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters' is missing"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty"); + } + + Optional hashAlgorithm = HashAlgorithm.fromString(sessionSignatureAlgorithmParameters.getHashAlgorithm()); + if (hashAlgorithm.isEmpty()) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: {}", sessionSignatureAlgorithmParameters.getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value"); + } + + var maskGenAlgorithm = sessionSignatureAlgorithmParameters.getMaskGenAlgorithm(); + if (maskGenAlgorithm == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty"); + } + + if (!MaskGenAlgorithm.ID_MGF1.getAlgorithmName().equals(maskGenAlgorithm.getAlgorithm())) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has invalid value: {}", maskGenAlgorithm.getAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value"); + } + + if (maskGenAlgorithm.getParameters() == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing"); + } + + if (StringUtil.isEmpty(maskGenAlgorithm.getParameters().getHashAlgorithm())) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty"); + } + + Optional mgfHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); + if (mgfHashAlgorithm.isEmpty()) { + logger.error("Signature session 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); + } + + if (!hashAlgorithm.get().equals(mgfHashAlgorithm.get())) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value. Expected {}, got {}", + hashAlgorithm.get().getAlgorithmName(), mgfHashAlgorithm.get().getAlgorithmName()); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + } + + if (sessionSignatureAlgorithmParameters.getSaltLength() == null) { + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing"); + } + + int expectedSaltLength = hashAlgorithm.get().getOctetLength(); + int actualSaltLength = sessionSignatureAlgorithmParameters.getSaltLength(); + if (expectedSaltLength != actualSaltLength) { + logger.error("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected {}, got {}", expectedSaltLength, actualSaltLength); + throw new UnprocessableSmartIdResponseException("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value"); + } + + if (StringUtil.isEmpty(sessionSignatureAlgorithmParameters.getTrailerField())) { + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty"); + } + + if (!TrailerField.BC.getValue().equals(sessionSignatureAlgorithmParameters.getTrailerField())) { + logger.error("Signature status field `signature.signatureAlgorithmParameters.trailerField` has invalid value: {}", sessionSignatureAlgorithmParameters.getTrailerField()); + throw new UnprocessableSmartIdResponseException("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidator.java b/src/main/java/ee/sk/smartid/SignatureValueValidator.java index 0197d0ac..5df475af 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidator.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidator.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for signature value validator. - */ -public interface SignatureValueValidator { - - /** - * Validates the signature value against the calculated signature value. - * - * @param signatureValue the signature value to validate - * @param payload the original data that was signed - * @param certificate X509 certificate used for signature validation - * @param rsaSsaPssParameters signature parameters used for creating signature value - * @throws UnprocessableSmartIdResponseException when there are any issue with validating the signature value - */ - void validate(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for signature value validator. + */ +public interface SignatureValueValidator { + + /** + * Validates the signature value against the calculated signature value. + * + * @param signatureValue the signature value to validate + * @param payload the original data that was signed + * @param certificate X509 certificate used for signature validation + * @param rsaSsaPssParameters signature parameters used for creating signature value + * @throws UnprocessableSmartIdResponseException when there are any issue with validating the signature value + */ + void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters); +} diff --git a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java index ae51a066..678f5f9d 100644 --- a/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java +++ b/src/main/java/ee/sk/smartid/SignatureValueValidatorImpl.java @@ -1,104 +1,104 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.cert.X509Certificate; -import java.security.spec.MGF1ParameterSpec; -import java.security.spec.PSSParameterSpec; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Implementation of {@link SignatureValueValidator} that uses RSASSA-PSS signature algorithm - * to validate the signature value in the authentication and signature session status response. - */ -public final class SignatureValueValidatorImpl implements SignatureValueValidator { - - private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); - - @Override - public void validate(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters) { - validateInputs(signatureValue, payload, certificate, rsaSsaPssParameters); - try { - Signature result = getSignature(rsaSsaPssParameters); - result.initVerify(certificate.getPublicKey()); - result.update(payload); - if (!result.verify(signatureValue)) { - throw new UnprocessableSmartIdResponseException("Provided signature value does not match the calculated signature value"); - } - } catch (GeneralSecurityException ex) { - throw new UnprocessableSmartIdResponseException("Signature value validation failed", ex); - } - } - - private Signature getSignature(RsaSsaPssParameters rsaSsaPssParameters) { - try { - var params = new PSSParameterSpec(rsaSsaPssParameters.getDigestHashAlgorithm().getAlgorithmName(), - rsaSsaPssParameters.getMaskGenAlgorithm().getMgfName(), - new MGF1ParameterSpec(rsaSsaPssParameters.getMaskHashAlgorithm().getAlgorithmName()), - rsaSsaPssParameters.getSaltLength(), - rsaSsaPssParameters.getTrailerField().getPssSpecValue()); - var signature = Signature.getInstance(rsaSsaPssParameters.getSignatureAlgorithm().getAlgorithmName()); - signature.setParameter(params); - return signature; - } catch (NoSuchAlgorithmException ex) { - logger.error("Invalid signature algorithm was provided: {}", rsaSsaPssParameters.getSignatureAlgorithm()); - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); - } catch (InvalidAlgorithmParameterException ex) { - throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); - } - } - - private static void validateInputs(byte[] signatureValue, - byte[] payload, - X509Certificate certificate, - RsaSsaPssParameters rsaSsaPssParameters) { - if (signatureValue == null) { - throw new SmartIdClientException("Parameter 'signatureValue' is not provided"); - } - if (payload == null) { - throw new SmartIdClientException("Parameter 'payload' is not provided"); - } - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - if (rsaSsaPssParameters == null) { - throw new SmartIdClientException("Parameter 'rsaSsaPssParameters' is not provided"); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Implementation of {@link SignatureValueValidator} that uses RSASSA-PSS signature algorithm + * to validate the signature value in the authentication and signature session status response. + */ +public final class SignatureValueValidatorImpl implements SignatureValueValidator { + + private final Logger logger = LoggerFactory.getLogger(SignatureValueValidatorImpl.class); + + @Override + public void validate(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + validateInputs(signatureValue, payload, certificate, rsaSsaPssParameters); + try { + Signature result = getSignature(rsaSsaPssParameters); + result.initVerify(certificate.getPublicKey()); + result.update(payload); + if (!result.verify(signatureValue)) { + throw new UnprocessableSmartIdResponseException("Provided signature value does not match the calculated signature value"); + } + } catch (GeneralSecurityException ex) { + throw new UnprocessableSmartIdResponseException("Signature value validation failed", ex); + } + } + + private Signature getSignature(RsaSsaPssParameters rsaSsaPssParameters) { + try { + var params = new PSSParameterSpec(rsaSsaPssParameters.getDigestHashAlgorithm().getAlgorithmName(), + rsaSsaPssParameters.getMaskGenAlgorithm().getMgfName(), + new MGF1ParameterSpec(rsaSsaPssParameters.getMaskHashAlgorithm().getAlgorithmName()), + rsaSsaPssParameters.getSaltLength(), + rsaSsaPssParameters.getTrailerField().getPssSpecValue()); + var signature = Signature.getInstance(rsaSsaPssParameters.getSignatureAlgorithm().getAlgorithmName()); + signature.setParameter(params); + return signature; + } catch (NoSuchAlgorithmException ex) { + logger.error("Invalid signature algorithm was provided: {}", rsaSsaPssParameters.getSignatureAlgorithm()); + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm was provided", ex); + } catch (InvalidAlgorithmParameterException ex) { + throw new UnprocessableSmartIdResponseException("Invalid signature algorithm parameters were provided", ex); + } + } + + private static void validateInputs(byte[] signatureValue, + byte[] payload, + X509Certificate certificate, + RsaSsaPssParameters rsaSsaPssParameters) { + if (signatureValue == null) { + throw new SmartIdClientException("Parameter 'signatureValue' is not provided"); + } + if (payload == null) { + throw new SmartIdClientException("Parameter 'payload' is not provided"); + } + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + if (rsaSsaPssParameters == null) { + throw new SmartIdClientException("Parameter 'rsaSsaPssParameters' is not provided"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/SmartIdClient.java b/src/main/java/ee/sk/smartid/SmartIdClient.java index 89a0a25c..a3ba1def 100644 --- a/src/main/java/ee/sk/smartid/SmartIdClient.java +++ b/src/main/java/ee/sk/smartid/SmartIdClient.java @@ -1,412 +1,412 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.Configuration; - -/** - * Main entry point for using Smart-ID services. - */ -public class SmartIdClient { - - private String relyingPartyUUID; - private String relyingPartyName; - private String hostUrl; - private Configuration networkConnectionConfig; - private Client configuredClient; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - private long sessionStatusResponseSocketOpenTimeValue; - private SmartIdConnector connector; - private SSLContext trustSslContext; - - private SessionStatusPoller sessionStatusPoller; - - /** - * Creates a new builder for creating a device link certificate choice session request. - * - * @return a builder for creating a new device link certificate choice session request - */ - public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertificateRequest() { - return new DeviceLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a linked notification signature session request. - * - * @return a builder for creating a new linked notification signature session request - */ - public LinkedNotificationSignatureSessionRequestBuilder createLinkedNotificationSignature() { - return new LinkedNotificationSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a notification certificate choice session request. - * - * @return a builder for creating a new notification certificate choice session request - */ - public NotificationCertificateChoiceSessionRequestBuilder createNotificationCertificateChoice() { - return new NotificationCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new device link authentication session request - * - * @return builder for creating a new device link authentication session request - */ - public DeviceLinkAuthenticationSessionRequestBuilder createDeviceLinkAuthentication() { - return new DeviceLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new notification authentication session request - * - * @return builder for creating a new notification authentication session request - */ - public NotificationAuthenticationSessionRequestBuilder createNotificationAuthentication() { - return new NotificationAuthenticationSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new device link signature session request - * - * @return builder for creating a new device link signature session request - */ - public DeviceLinkSignatureSessionRequestBuilder createDeviceLinkSignature() { - return new DeviceLinkSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for requesting a certificate using document number. - * - * @return builder for querying certificate using document number - */ - public CertificateByDocumentNumberRequestBuilder createCertificateByDocumentNumber() { - return new CertificateByDocumentNumberRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Creates a new builder for creating a new notification signature session request - * - * @return builder for creating a new notification signature session request - */ - public NotificationSignatureSessionRequestBuilder createNotificationSignature() { - return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()) - .withRelyingPartyUUID(relyingPartyUUID) - .withRelyingPartyName(relyingPartyName); - } - - /** - * Returns the session status poller or creates a new one if it doesn't exist - * - * @return Sessions status poller - */ - public SessionStatusPoller getSessionStatusPoller() { - if (sessionStatusPoller == null) { - sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); - sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); - } - return sessionStatusPoller; - } - - /** - * Create builder for generating device link or QR-code - * - * @return DeviceLinkBuilder - * @throws SmartIdClientException if required parameters are missing or invalid - */ - public DeviceLinkBuilder createDynamicContent() { - return new DeviceLinkBuilder() - .withRelyingPartyName(relyingPartyName); - } - - /** - * Sets the UUID of the relying party - *

- * Can be set also on the builder level, - * but in that case it has to be set explicitly - * every time when building a new request. - * - * @param relyingPartyUUID UUID of the relying party - */ - public void setRelyingPartyUUID(String relyingPartyUUID) { - this.relyingPartyUUID = relyingPartyUUID; - } - - /** - * Gets the UUID of the relying party - * - * @return UUID of the relying party - */ - public String getRelyingPartyUUID() { - return relyingPartyUUID; - } - - /** - * Sets the name of the relying party - *

- * Can be set also on the builder level, - * but in that case it has to be set - * every time when building a new request. - * - * @param relyingPartyName name of the relying party - */ - public void setRelyingPartyName(String relyingPartyName) { - this.relyingPartyName = relyingPartyName; - } - - /** - * Gets the name of the relying party - * - * @return name of the relying party - */ - public String getRelyingPartyName() { - return relyingPartyName; - } - - /** - * Sets the base URL of the Smart-ID backend environment - *

- * It defines the endpoint which the client communicates to. - * - * @param hostUrl base URL of the Smart-ID backend environment - */ - public void setHostUrl(String hostUrl) { - this.hostUrl = hostUrl; - } - - /** - * Sets the network connection configuration - *

- * Useful for configuring network connection - * timeouts, proxy settings, request headers etc. - * - * @param networkConnectionConfig Jersey's network connection configuration instance - */ - public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { - this.networkConnectionConfig = networkConnectionConfig; - } - - /** - * Set the configured client. - * - * @param configuredClient jakarta.ws.rs.client.Client implementations - */ - public void setConfiguredClient(Client configuredClient) { - this.configuredClient = configuredClient; - } - - /** - * Sets the timeout for each session status poll - *

- * Under the hood each operation (authentication, signing, choosing - * certificate) consists of 2 request steps: - *

- * 1. Initiation request - *

- * 2. Session status request - *

- * Session status request is a long poll method, meaning - * the request method might not return until a timeout expires - * set by this parameter. - *

- * Caller can tune the request parameters inside the bounds - * set by service operator. - *

- * If not provided, a default is used. - * - * @param timeUnit time unit of the {@code timeValue} argument - * @param timeValue time value of each status poll's timeout. - */ - public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - sessionStatusResponseSocketOpenTimeUnit = timeUnit; - sessionStatusResponseSocketOpenTimeValue = timeValue; - } - - /** - * Sets the timeout/pause between each session status poll - * - * @param unit time unit of the {@code timeout} argument - * @param timeout timeout value in the given {@code unit} - */ - public void setPollingSleepTimeout(TimeUnit unit, long timeout) { - pollingSleepTimeUnit = unit; - pollingSleepTimeout = timeout; - } - - /** - * Get smart-id connector. If connector is not set, then new will be created - * - * @return smart-id connector - */ - public SmartIdConnector getSmartIdConnector() { - if (null == connector) { - Client client = configuredClient != null ? configuredClient : createClient(); - SmartIdRestConnector connector = new SmartIdRestConnector(hostUrl, client); - connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - - if (trustSslContext == null && configuredClient == null) { - throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); - } - - connector.setSslContext(this.trustSslContext); - setSmartIdConnector(connector); - } - return connector; - } - - /** - * Sets the SSL context for the client - *

- * Useful for configuring custom SSL context - * for the client. - * - * @param trustSslContext SSL context for the client - */ - public void setTrustSslContext(SSLContext trustSslContext) { - this.trustSslContext = trustSslContext; - } - - /** - * Set trust store containing SSL certificates - * - * @param trustStore trust store for the client - */ - public void setTrustStore(KeyStore trustStore) { - try { - SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(trustStore); - trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); - this.trustSslContext = trustSslContext; - } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); - } - } - - /** - * Can be used instead of {@link #setTrustStore} to add SSL certificates. - * - * @param sslCertificates certificates in PEM format - */ - public void setTrustedCertificates(String... sslCertificates) { - try { - this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); - } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { - throw new SmartIdClientException("Failed to createSslContext", e); - } - } - - /** - * Sets the smart-id connector - *

- * Useful for providing custom implementation - * of the connector. - * - * @param smartIdConnector smart-id connector - */ - public void setSmartIdConnector(SmartIdConnector smartIdConnector) { - this.connector = smartIdConnector; - } - - private Client createClient() { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (networkConnectionConfig != null) { - clientBuilder.withConfig(networkConnectionConfig); - } - if (trustSslContext != null) { - clientBuilder.sslContext(trustSslContext); - } - return clientBuilder.build(); - } - - /** - * Creates an SSL context with the given certificates - * - * @param sslCertificates list of certificates in PEM format - * @return SSL context - * @throws NoSuchAlgorithmException if SSL context with provided protocol is not found - * @throws KeyStoreException if key store cannot be created for a type - * @throws IOException if loading key store fails - * @throws CertificateException if certificate for the given data cannot be generated - * @throws KeyManagementException if SSL context cannot be initialized - */ - public static SSLContext createSslContext(List sslCertificates) - throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { - SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(null); - CertificateFactory factory = CertificateFactory.getInstance("X509"); - int i = 0; - for (String sslCertificate : sslCertificates) { - Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); - keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); - } - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); - trustManagerFactory.init(keyStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Configuration; + +/** + * Main entry point for using Smart-ID services. + */ +public class SmartIdClient { + + private String relyingPartyUUID; + private String relyingPartyName; + private String hostUrl; + private Configuration networkConnectionConfig; + private Client configuredClient; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + private long sessionStatusResponseSocketOpenTimeValue; + private SmartIdConnector connector; + private SSLContext trustSslContext; + + private SessionStatusPoller sessionStatusPoller; + + /** + * Creates a new builder for creating a device link certificate choice session request. + * + * @return a builder for creating a new device link certificate choice session request + */ + public DeviceLinkCertificateChoiceSessionRequestBuilder createDeviceLinkCertificateRequest() { + return new DeviceLinkCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a linked notification signature session request. + * + * @return a builder for creating a new linked notification signature session request + */ + public LinkedNotificationSignatureSessionRequestBuilder createLinkedNotificationSignature() { + return new LinkedNotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a notification certificate choice session request. + * + * @return a builder for creating a new notification certificate choice session request + */ + public NotificationCertificateChoiceSessionRequestBuilder createNotificationCertificateChoice() { + return new NotificationCertificateChoiceSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new device link authentication session request + * + * @return builder for creating a new device link authentication session request + */ + public DeviceLinkAuthenticationSessionRequestBuilder createDeviceLinkAuthentication() { + return new DeviceLinkAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new notification authentication session request + * + * @return builder for creating a new notification authentication session request + */ + public NotificationAuthenticationSessionRequestBuilder createNotificationAuthentication() { + return new NotificationAuthenticationSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new device link signature session request + * + * @return builder for creating a new device link signature session request + */ + public DeviceLinkSignatureSessionRequestBuilder createDeviceLinkSignature() { + return new DeviceLinkSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for requesting a certificate using document number. + * + * @return builder for querying certificate using document number + */ + public CertificateByDocumentNumberRequestBuilder createCertificateByDocumentNumber() { + return new CertificateByDocumentNumberRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Creates a new builder for creating a new notification signature session request + * + * @return builder for creating a new notification signature session request + */ + public NotificationSignatureSessionRequestBuilder createNotificationSignature() { + return new NotificationSignatureSessionRequestBuilder(getSmartIdConnector()) + .withRelyingPartyUUID(relyingPartyUUID) + .withRelyingPartyName(relyingPartyName); + } + + /** + * Returns the session status poller or creates a new one if it doesn't exist + * + * @return Sessions status poller + */ + public SessionStatusPoller getSessionStatusPoller() { + if (sessionStatusPoller == null) { + sessionStatusPoller = new SessionStatusPoller(getSmartIdConnector()); + sessionStatusPoller.setPollingSleepTime(pollingSleepTimeUnit, pollingSleepTimeout); + } + return sessionStatusPoller; + } + + /** + * Create builder for generating device link or QR-code + * + * @return DeviceLinkBuilder + * @throws SmartIdClientException if required parameters are missing or invalid + */ + public DeviceLinkBuilder createDynamicContent() { + return new DeviceLinkBuilder() + .withRelyingPartyName(relyingPartyName); + } + + /** + * Sets the UUID of the relying party + *

+ * Can be set also on the builder level, + * but in that case it has to be set explicitly + * every time when building a new request. + * + * @param relyingPartyUUID UUID of the relying party + */ + public void setRelyingPartyUUID(String relyingPartyUUID) { + this.relyingPartyUUID = relyingPartyUUID; + } + + /** + * Gets the UUID of the relying party + * + * @return UUID of the relying party + */ + public String getRelyingPartyUUID() { + return relyingPartyUUID; + } + + /** + * Sets the name of the relying party + *

+ * Can be set also on the builder level, + * but in that case it has to be set + * every time when building a new request. + * + * @param relyingPartyName name of the relying party + */ + public void setRelyingPartyName(String relyingPartyName) { + this.relyingPartyName = relyingPartyName; + } + + /** + * Gets the name of the relying party + * + * @return name of the relying party + */ + public String getRelyingPartyName() { + return relyingPartyName; + } + + /** + * Sets the base URL of the Smart-ID backend environment + *

+ * It defines the endpoint which the client communicates to. + * + * @param hostUrl base URL of the Smart-ID backend environment + */ + public void setHostUrl(String hostUrl) { + this.hostUrl = hostUrl; + } + + /** + * Sets the network connection configuration + *

+ * Useful for configuring network connection + * timeouts, proxy settings, request headers etc. + * + * @param networkConnectionConfig Jersey's network connection configuration instance + */ + public void setNetworkConnectionConfig(Configuration networkConnectionConfig) { + this.networkConnectionConfig = networkConnectionConfig; + } + + /** + * Set the configured client. + * + * @param configuredClient jakarta.ws.rs.client.Client implementations + */ + public void setConfiguredClient(Client configuredClient) { + this.configuredClient = configuredClient; + } + + /** + * Sets the timeout for each session status poll + *

+ * Under the hood each operation (authentication, signing, choosing + * certificate) consists of 2 request steps: + *

+ * 1. Initiation request + *

+ * 2. Session status request + *

+ * Session status request is a long poll method, meaning + * the request method might not return until a timeout expires + * set by this parameter. + *

+ * Caller can tune the request parameters inside the bounds + * set by service operator. + *

+ * If not provided, a default is used. + * + * @param timeUnit time unit of the {@code timeValue} argument + * @param timeValue time value of each status poll's timeout. + */ + public void setSessionStatusResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + sessionStatusResponseSocketOpenTimeUnit = timeUnit; + sessionStatusResponseSocketOpenTimeValue = timeValue; + } + + /** + * Sets the timeout/pause between each session status poll + * + * @param unit time unit of the {@code timeout} argument + * @param timeout timeout value in the given {@code unit} + */ + public void setPollingSleepTimeout(TimeUnit unit, long timeout) { + pollingSleepTimeUnit = unit; + pollingSleepTimeout = timeout; + } + + /** + * Get smart-id connector. If connector is not set, then new will be created + * + * @return smart-id connector + */ + public SmartIdConnector getSmartIdConnector() { + if (null == connector) { + Client client = configuredClient != null ? configuredClient : createClient(); + SmartIdRestConnector connector = new SmartIdRestConnector(hostUrl, client); + connector.setSessionStatusResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + + if (trustSslContext == null && configuredClient == null) { + throw new SmartIdClientException("You must provide trusted API server certificates either by calling setTrustStore(), setTrustedCertificates() or setTrustSslContext() or setConfiguredClient()"); + } + + connector.setSslContext(this.trustSslContext); + setSmartIdConnector(connector); + } + return connector; + } + + /** + * Sets the SSL context for the client + *

+ * Useful for configuring custom SSL context + * for the client. + * + * @param trustSslContext SSL context for the client + */ + public void setTrustSslContext(SSLContext trustSslContext) { + this.trustSslContext = trustSslContext; + } + + /** + * Set trust store containing SSL certificates + * + * @param trustStore trust store for the client + */ + public void setTrustStore(KeyStore trustStore) { + try { + SSLContext trustSslContext = SSLContext.getInstance("TLSv1.2"); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(trustStore); + trustSslContext.init(null, trustManagerFactory.getTrustManagers(), null); + this.trustSslContext = trustSslContext; + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Problem with supplied trust store file: " + e.getMessage()); + } + } + + /** + * Can be used instead of {@link #setTrustStore} to add SSL certificates. + * + * @param sslCertificates certificates in PEM format + */ + public void setTrustedCertificates(String... sslCertificates) { + try { + this.trustSslContext = createSslContext(Arrays.asList(sslCertificates)); + } catch (CertificateException | IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + throw new SmartIdClientException("Failed to createSslContext", e); + } + } + + /** + * Sets the smart-id connector + *

+ * Useful for providing custom implementation + * of the connector. + * + * @param smartIdConnector smart-id connector + */ + public void setSmartIdConnector(SmartIdConnector smartIdConnector) { + this.connector = smartIdConnector; + } + + private Client createClient() { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (networkConnectionConfig != null) { + clientBuilder.withConfig(networkConnectionConfig); + } + if (trustSslContext != null) { + clientBuilder.sslContext(trustSslContext); + } + return clientBuilder.build(); + } + + /** + * Creates an SSL context with the given certificates + * + * @param sslCertificates list of certificates in PEM format + * @return SSL context + * @throws NoSuchAlgorithmException if SSL context with provided protocol is not found + * @throws KeyStoreException if key store cannot be created for a type + * @throws IOException if loading key store fails + * @throws CertificateException if certificate for the given data cannot be generated + * @throws KeyManagementException if SSL context cannot be initialized + */ + public static SSLContext createSslContext(List sslCertificates) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + int i = 0; + for (String sslCertificate : sslCertificates) { + Certificate certificate = factory.generateCertificate(new ByteArrayInputStream(sslCertificate.getBytes(StandardCharsets.UTF_8))); + keyStore.setCertificateEntry("sid_api_ssl_cert_" + (++i), certificate); + } + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509"); + trustManagerFactory.init(keyStore); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext; + } +} diff --git a/src/main/java/ee/sk/smartid/TrailerField.java b/src/main/java/ee/sk/smartid/TrailerField.java index 394968da..b8482bb8 100644 --- a/src/main/java/ee/sk/smartid/TrailerField.java +++ b/src/main/java/ee/sk/smartid/TrailerField.java @@ -1,81 +1,81 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; - -/** - * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response - * and related value for PSSParameterSpec. - */ -public enum TrailerField { - - /** - * Trailer field hexadecimal value "0xbc" with PSSParameterSpec value 1. - */ - BC("0xbc", 1); - - private final String value; - private final int pssSpecValue; - - TrailerField(String value, int pssSpecValue) { - this.value = value; - this.pssSpecValue = pssSpecValue; - } - - /** - * Gets the hexadecimal value of the trailer field. - * - * @return the hexadecimal value of the trailer field. - */ - public String getValue() { - return value; - } - - /** - * Gets the PSSParameterSpec value associated with the trailer field. - * - * @return the PSS specification value. - */ - public int getPssSpecValue() { - return pssSpecValue; - } - - /** - * Converts a string representation of a trailer field to its corresponding TrailerField enum value. - * - * @param trailerField the string representation of the trailer field. - * @return the corresponding TrailerField enum value. - * @throws IllegalArgumentException if the provided string does not match any TrailerField value. - */ - public static TrailerField fromString(String trailerField) { - return Arrays.stream(TrailerField.values()) - .filter(field -> field.getValue().equals(trailerField)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Invalid trailerField value: " + trailerField)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; + +/** + * TrailerField represents the value used in the trailer field of the Smart-ID authentication and signature response + * and related value for PSSParameterSpec. + */ +public enum TrailerField { + + /** + * Trailer field hexadecimal value "0xbc" with PSSParameterSpec value 1. + */ + BC("0xbc", 1); + + private final String value; + private final int pssSpecValue; + + TrailerField(String value, int pssSpecValue) { + this.value = value; + this.pssSpecValue = pssSpecValue; + } + + /** + * Gets the hexadecimal value of the trailer field. + * + * @return the hexadecimal value of the trailer field. + */ + public String getValue() { + return value; + } + + /** + * Gets the PSSParameterSpec value associated with the trailer field. + * + * @return the PSS specification value. + */ + public int getPssSpecValue() { + return pssSpecValue; + } + + /** + * Converts a string representation of a trailer field to its corresponding TrailerField enum value. + * + * @param trailerField the string representation of the trailer field. + * @return the corresponding TrailerField enum value. + * @throws IllegalArgumentException if the provided string does not match any TrailerField value. + */ + public static TrailerField fromString(String trailerField) { + return Arrays.stream(TrailerField.values()) + .filter(field -> field.getValue().equals(trailerField)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid trailerField value: " + trailerField)); + } +} diff --git a/src/main/java/ee/sk/smartid/TrustedCACertStore.java b/src/main/java/ee/sk/smartid/TrustedCACertStore.java index eaef9356..c2ebc210 100644 --- a/src/main/java/ee/sk/smartid/TrustedCACertStore.java +++ b/src/main/java/ee/sk/smartid/TrustedCACertStore.java @@ -1,59 +1,59 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -/** - * Interface for a store of trusted CA certificates and trust anchors. - */ -public interface TrustedCACertStore { - - /** - * Get a list of all trusted CA certificates. - * - * @return copy of trusted CA certificates - */ - List getTrustedCACertificates(); - - /** - * Get a set of all trust anchors. - * - * @return copy of trust anchors - */ - Set getTrustAnchors(); - - /** - * Check if OCSP (Online Certificate Status Protocol) validation is enabled. - * - * @return true if OCSP validation is enabled, false otherwise - */ - boolean isOcspEnabled(); -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +/** + * Interface for a store of trusted CA certificates and trust anchors. + */ +public interface TrustedCACertStore { + + /** + * Get a list of all trusted CA certificates. + * + * @return copy of trusted CA certificates + */ + List getTrustedCACertificates(); + + /** + * Get a set of all trust anchors. + * + * @return copy of trust anchors + */ + Set getTrustAnchors(); + + /** + * Check if OCSP (Online Certificate Status Protocol) validation is enabled. + * + * @return true if OCSP validation is enabled, false otherwise + */ + boolean isOcspEnabled(); +} diff --git a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java index 00ca2d81..661658f5 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeCalculator.java @@ -1,65 +1,65 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.ByteBuffer; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for calculating verification code from a hash. - */ -public class VerificationCodeCalculator { - - /** - * The Verification Code (VC) is computed as: - *

- * integer(SHA256(data)[−2:−1]) mod 10000 - *

- * where we take SHA256 result, extract 2 rightmost bytes from it, - * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. - *

- * SHA256 is always used here - * - * @param data byte array to calculate verification code from - * @return verification code. - */ - public static String calculate(byte[] data) { - if (data == null || data.length == 0) { - throw new SmartIdClientException("Parameter 'data' cannot be empty"); - } - byte[] digest = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); - ByteBuffer byteBuffer = ByteBuffer.wrap(digest); - int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 - int rightMostBytesIndex = byteBuffer.limit() - shortBytes; - short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); - int positiveInteger = ((int) twoRightmostBytes) & 0xffff; - String code = String.valueOf(positiveInteger); - String paddedCode = "0000" + code; - return paddedCode.substring(code.length()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.ByteBuffer; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for calculating verification code from a hash. + */ +public class VerificationCodeCalculator { + + /** + * The Verification Code (VC) is computed as: + *

+ * integer(SHA256(data)[−2:−1]) mod 10000 + *

+ * where we take SHA256 result, extract 2 rightmost bytes from it, + * interpret them as a big-endian unsigned integer and take the last 4 digits in decimal for display. + *

+ * SHA256 is always used here + * + * @param data byte array to calculate verification code from + * @return verification code. + */ + public static String calculate(byte[] data) { + if (data == null || data.length == 0) { + throw new SmartIdClientException("Parameter 'data' cannot be empty"); + } + byte[] digest = DigestCalculator.calculateDigest(data, HashAlgorithm.SHA_256); + ByteBuffer byteBuffer = ByteBuffer.wrap(digest); + int shortBytes = Short.SIZE / Byte.SIZE; // Short.BYTES in java 8 + int rightMostBytesIndex = byteBuffer.limit() - shortBytes; + short twoRightmostBytes = byteBuffer.getShort(rightMostBytesIndex); + int positiveInteger = ((int) twoRightmostBytes) & 0xffff; + String code = String.valueOf(positiveInteger); + String paddedCode = "0000" + code; + return paddedCode.substring(code.length()); + } +} diff --git a/src/main/java/ee/sk/smartid/VerificationCodeType.java b/src/main/java/ee/sk/smartid/VerificationCodeType.java index d542abc1..9f5b673f 100644 --- a/src/main/java/ee/sk/smartid/VerificationCodeType.java +++ b/src/main/java/ee/sk/smartid/VerificationCodeType.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Verification code types to be used with notification-based authentication request and signing session response - */ -public enum VerificationCodeType { - - NUMERIC4("numeric4"); - - private final String value; - - VerificationCodeType(String value) { - this.value = value; - } - - /** - * Returns the string representation of the verification code type. - * - * @return the string value of the verification code type - */ - public String getValue(){ - return value; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Verification code types to be used with notification-based authentication request and signing session response + */ +public enum VerificationCodeType { + + NUMERIC4("numeric4"); + + private final String value; + + VerificationCodeType(String value) { + this.value = value; + } + + /** + * Returns the string representation of the verification code type. + * + * @return the string value of the verification code type + */ + public String getValue(){ + return value; + } +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java index 6860047f..4e48ff03 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidator.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Interface for validating whether a given X509 certificate is suitable for authentication purposes. - * Implementations should check certificate properties and throw an exception if the certificate is not valid for authentication. - */ -public interface AuthenticationCertificatePurposeValidator { - - /** - * Validates that the provided certificate is suitable for authentication - * - * @param certificate certificate to validate - * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for authentication - */ - void validate(X509Certificate certificate); -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Interface for validating whether a given X509 certificate is suitable for authentication purposes. + * Implementations should check certificate properties and throw an exception if the certificate is not valid for authentication. + */ +public interface AuthenticationCertificatePurposeValidator { + + /** + * Validates that the provided certificate is suitable for authentication + * + * @param certificate certificate to validate + * @throws UnprocessableSmartIdResponseException when the certificate is not suitable for authentication + */ + void validate(X509Certificate certificate); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java index 9e183c64..a35d5b3f 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactory.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.AuthenticationCertificateLevel; - -/** - * Factory interface for creating {@link AuthenticationCertificatePurposeValidator} instances - */ -public interface AuthenticationCertificatePurposeValidatorFactory { - - /** - * Creates an {@link AuthenticationCertificatePurposeValidator} for the specified certificate level. - * - * @param certificateLevel the level of the authentication certificate - * @return an instance of {@link AuthenticationCertificatePurposeValidator} suitable for the given level - */ - AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel); -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory interface for creating {@link AuthenticationCertificatePurposeValidator} instances + */ +public interface AuthenticationCertificatePurposeValidatorFactory { + + /** + * Creates an {@link AuthenticationCertificatePurposeValidator} for the specified certificate level. + * + * @param certificateLevel the level of the authentication certificate + * @return an instance of {@link AuthenticationCertificatePurposeValidator} suitable for the given level + */ + AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel); +} diff --git a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java index 3b177eae..6e23507e 100644 --- a/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java +++ b/src/main/java/ee/sk/smartid/auth/AuthenticationCertificatePurposeValidatorFactoryImpl.java @@ -1,50 +1,50 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.AuthenticationCertificateLevel; - -/** - * Factory implementation for creating {@link AuthenticationCertificatePurposeValidator} - * instances based on the provided {@link AuthenticationCertificateLevel}. - *

- * Returns a validator suitable for the certificate level: - *

    - *
  • {@code QUALIFIED} - returns {@link QualifiedAuthenticationCertificatePurposeValidator}
  • - *
  • {@code ADVANCED} - returns {@link NonQualifiedAuthenticationCertificatePurposeValidator}
  • - *
- */ -public class AuthenticationCertificatePurposeValidatorFactoryImpl implements AuthenticationCertificatePurposeValidatorFactory { - - @Override - public AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel) { - return switch (certificateLevel) { - case QUALIFIED -> new QualifiedAuthenticationCertificatePurposeValidator(); - case ADVANCED -> new NonQualifiedAuthenticationCertificatePurposeValidator(); - }; - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.AuthenticationCertificateLevel; + +/** + * Factory implementation for creating {@link AuthenticationCertificatePurposeValidator} + * instances based on the provided {@link AuthenticationCertificateLevel}. + *

+ * Returns a validator suitable for the certificate level: + *

    + *
  • {@code QUALIFIED} - returns {@link QualifiedAuthenticationCertificatePurposeValidator}
  • + *
  • {@code ADVANCED} - returns {@link NonQualifiedAuthenticationCertificatePurposeValidator}
  • + *
+ */ +public class AuthenticationCertificatePurposeValidatorFactoryImpl implements AuthenticationCertificatePurposeValidatorFactory { + + @Override + public AuthenticationCertificatePurposeValidator create(AuthenticationCertificateLevel certificateLevel) { + return switch (certificateLevel) { + case QUALIFIED -> new QualifiedAuthenticationCertificatePurposeValidator(); + case ADVANCED -> new NonQualifiedAuthenticationCertificatePurposeValidator(); + }; + } +} diff --git a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java index d627eb2a..41a03d63 100644 --- a/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidator.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; - -import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; -import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Validator for non-qualified Smart-ID authentication certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile - *

- * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. - */ -public class NonQualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { - - @Override - public void validate(X509Certificate certificate) { - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - NonQualifiedSmartIdCertificateValidator.validate(certificate); - SmartIdAuthenticationCertificateValidator.validate(certificate); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; + +import ee.sk.smartid.common.certificate.NonQualifiedSmartIdCertificateValidator; +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Validator for non-qualified Smart-ID authentication certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Non-Qualified Digital Signature + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) for Non-Qualified profile + *

+ * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class NonQualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + NonQualifiedSmartIdCertificateValidator.validate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } +} diff --git a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java index 5ef6f239..1bcc7150 100644 --- a/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java +++ b/src/main/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidator.java @@ -1,77 +1,77 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validates that the authentication certificate is a qualified Smart-ID certificate and can be used for authentication. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Authentication - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (authentication) for Qualified profile - *

- * * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. - */ -public class QualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { - - private final Logger logger = LoggerFactory.getLogger(QualifiedAuthenticationCertificatePurposeValidator.class); - - private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.2042.1.2"); - - @Override - public void validate(X509Certificate certificate) { - if (certificate == null) { - throw new SmartIdClientException("Parameter 'certificate' is not provided"); - } - validateCertificateIsQualifiedSmartIdCertificate(certificate); - SmartIdAuthenticationCertificateValidator.validate(certificate); - } - - private void validateCertificateIsQualifiedSmartIdCertificate(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate"); - } - if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate is not a qualified Smart-ID authentication certificate"); - } - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.common.certificate.SmartIdAuthenticationCertificateValidator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validates that the authentication certificate is a qualified Smart-ID certificate and can be used for authentication. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified Authentication + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (authentication) for Qualified profile + *

+ * * Throws {@link ee.sk.smartid.exception.UnprocessableSmartIdResponseException} if validation fails. + */ +public class QualifiedAuthenticationCertificatePurposeValidator implements AuthenticationCertificatePurposeValidator { + + private final Logger logger = LoggerFactory.getLogger(QualifiedAuthenticationCertificatePurposeValidator.class); + + private static final Set QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.2", "0.4.0.2042.1.2"); + + @Override + public void validate(X509Certificate certificate) { + if (certificate == null) { + throw new SmartIdClientException("Parameter 'certificate' is not provided"); + } + validateCertificateIsQualifiedSmartIdCertificate(certificate); + SmartIdAuthenticationCertificateValidator.validate(certificate); + } + + private void validateCertificateIsQualifiedSmartIdCertificate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate"); + } + if (!certificatePolicyOids.containsAll(QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a qualified Smart-ID authentication certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionType.java b/src/main/java/ee/sk/smartid/common/InteractionType.java index 54210156..4063127d 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionType.java +++ b/src/main/java/ee/sk/smartid/common/InteractionType.java @@ -1,47 +1,47 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Interface for interaction types that can be used in authentication and signing requests. - */ -public interface InteractionType { - - /** - * Get the interaction type as value that can be used in the Smart ID API. - * - * @return code representing the interaction type - */ - String getCode(); - - /** - * Get the maximum length of the display text for this interaction type. - * - * @return maximum length of the display text - */ - int getMaxLength(); -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Interface for interaction types that can be used in authentication and signing requests. + */ +public interface InteractionType { + + /** + * Get the interaction type as value that can be used in the Smart ID API. + * + * @return code representing the interaction type + */ + String getCode(); + + /** + * Get the maximum length of the display text for this interaction type. + * + * @return maximum length of the display text + */ + int getMaxLength(); +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionValidator.java b/src/main/java/ee/sk/smartid/common/InteractionValidator.java index 64036156..21873f3e 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionValidator.java +++ b/src/main/java/ee/sk/smartid/common/InteractionValidator.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.util.StringUtil; - -/** - * Validator for interactions - */ -public final class InteractionValidator { - - private InteractionValidator() { - } - - /** - * Validates that the text is set and does not exceed the maximum length defined by the type - * - * @param type the type to be validated - * @param text the text to be validated - * @param implementation of InteractionType - */ - public static void validate(T type, String text) { - if (StringUtil.isEmpty(text)) { - throw new SmartIdRequestSetupException(String.format("Value for '%s' must be set when type is '%s'", "displayText" + type.getMaxLength(), type)); - } - if (text.length() > type.getMaxLength()) { - throw new SmartIdRequestSetupException(String.format("Value for '%s' must not exceed %d characters", "displayText" + type.getMaxLength(), type.getMaxLength())); - } - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.util.StringUtil; + +/** + * Validator for interactions + */ +public final class InteractionValidator { + + private InteractionValidator() { + } + + /** + * Validates that the text is set and does not exceed the maximum length defined by the type + * + * @param type the type to be validated + * @param text the text to be validated + * @param implementation of InteractionType + */ + public static void validate(T type, String text) { + if (StringUtil.isEmpty(text)) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must be set when type is '%s'", "displayText" + type.getMaxLength(), type)); + } + if (text.length() > type.getMaxLength()) { + throw new SmartIdRequestSetupException(String.format("Value for '%s' must not exceed %d characters", "displayText" + type.getMaxLength(), type.getMaxLength())); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java index 175f1020..c56e52ae 100644 --- a/src/main/java/ee/sk/smartid/common/InteractionsMapper.java +++ b/src/main/java/ee/sk/smartid/common/InteractionsMapper.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.Objects; - -import ee.sk.smartid.rest.dao.Interaction; - -/** - * Mapper form converting between different interaction representations - */ -public final class InteractionsMapper { - - private InteractionsMapper() { - } - - /** - * Converts from any SmartIdInteraction to Interaction - * - * @param type of SmartIdInteraction - * @param interaction the interaction to be converted - * @return interaction to be used in REST request - */ - public static Interaction from(T interaction) { - return new Interaction(interaction.type().getCode(), interaction.displayText60(), interaction.displayText200()); - } - - /** - * Converts from any list of SmartIdInteraction to list of Interaction - * - * @param interactions the interactions to be converted - * @return list of interactions to be used in REST request - */ - public static List from(List interactions) { - return interactions.stream().filter(Objects::nonNull).map(InteractionsMapper::from).toList(); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.Objects; + +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Mapper form converting between different interaction representations + */ +public final class InteractionsMapper { + + private InteractionsMapper() { + } + + /** + * Converts from any SmartIdInteraction to Interaction + * + * @param type of SmartIdInteraction + * @param interaction the interaction to be converted + * @return interaction to be used in REST request + */ + public static Interaction from(T interaction) { + return new Interaction(interaction.type().getCode(), interaction.displayText60(), interaction.displayText200()); + } + + /** + * Converts from any list of SmartIdInteraction to list of Interaction + * + * @param interactions the interactions to be converted + * @return list of interactions to be used in REST request + */ + public static List from(List interactions) { + return interactions.stream().filter(Objects::nonNull).map(InteractionsMapper::from).toList(); + } +} diff --git a/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java index 108b035e..9a4e7d22 100644 --- a/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java +++ b/src/main/java/ee/sk/smartid/common/SmartIdInteraction.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Interaction to be used in authentication and signing requests - */ -public interface SmartIdInteraction { - - /** - * Gets the interaction type - * - * @return the interaction type - */ - InteractionType type(); - - /** - * Gets the text to be displayed on the device screen (maximum length 60 characters). - * - * @return the text to be displayed on the device screen (maximum length 60 characters). - */ - String displayText60(); - - /** - * Gets the text to be displayed on the device screen (maximum length 200 characters). - * - * @return the text to be displayed on the device screen (maximum length 200 characters). - */ - String displayText200(); -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Interaction to be used in authentication and signing requests + */ +public interface SmartIdInteraction { + + /** + * Gets the interaction type + * + * @return the interaction type + */ + InteractionType type(); + + /** + * Gets the text to be displayed on the device screen (maximum length 60 characters). + * + * @return the text to be displayed on the device screen (maximum length 60 characters). + */ + String displayText60(); + + /** + * Gets the text to be displayed on the device screen (maximum length 200 characters). + * + * @return the text to be displayed on the device screen (maximum length 200 characters). + */ + String displayText200(); +} diff --git a/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java index 3d38c38c..5dbf7389 100644 --- a/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/NonQualifiedSmartIdCertificateValidator.java @@ -1,72 +1,72 @@ -package ee.sk.smartid.common.certificate; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.X509Certificate; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.util.CertificateAttributeUtil; - -/** - * Validator for non-qualified Smart-ID certificates. Can be used for both authentication and signing certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) and (authentification) for Non-Qualified profile - */ -public final class NonQualifiedSmartIdCertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSmartIdCertificateValidator.class); - - private static final Set NON_QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); - - private NonQualifiedSmartIdCertificateValidator() { - } - - /** - * Validates that the provided certificate is a non-qualified Smart-ID certificate. - * - * @param certificate the certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate is not a non-qualified Smart-ID certificate - */ - public static void validate(X509Certificate certificate) { - Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); - if (certificatePolicyOids.isEmpty()) { - throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate"); - } - if (!certificatePolicyOids.containsAll(NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)) { - logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", - String.join(", ", certificatePolicyOids), - String.join(", ", NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)); - throw new UnprocessableSmartIdResponseException("Certificate is not a non-qualified Smart-ID certificate"); - } - } -} +package ee.sk.smartid.common.certificate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.util.CertificateAttributeUtil; + +/** + * Validator for non-qualified Smart-ID certificates. Can be used for both authentication and signing certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.3 Certificate Policy and section PolicyIdentifier (digital signature) and (authentification) for Non-Qualified profile + */ +public final class NonQualifiedSmartIdCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(NonQualifiedSmartIdCertificateValidator.class); + + private static final Set NON_QUALIFIED_CERTIFICATE_POLICY_OIDS = Set.of("1.3.6.1.4.1.10015.17.1", "0.4.0.2042.1.1"); + + private NonQualifiedSmartIdCertificateValidator() { + } + + /** + * Validates that the provided certificate is a non-qualified Smart-ID certificate. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate is not a non-qualified Smart-ID certificate + */ + public static void validate(X509Certificate certificate) { + Set certificatePolicyOids = CertificateAttributeUtil.getCertificatePolicy(certificate); + if (certificatePolicyOids.isEmpty()) { + throw new UnprocessableSmartIdResponseException("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate"); + } + if (!certificatePolicyOids.containsAll(NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)) { + logger.error("Qualified certificate policy OIDs are missing. Provided certificate policy OIDs: {}. Required: {} ", + String.join(", ", certificatePolicyOids), + String.join(", ", NON_QUALIFIED_CERTIFICATE_POLICY_OIDS)); + throw new UnprocessableSmartIdResponseException("Certificate is not a non-qualified Smart-ID certificate"); + } + } +} diff --git a/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java index 10d901ec..716045ab 100644 --- a/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java +++ b/src/main/java/ee/sk/smartid/common/certificate/SmartIdAuthenticationCertificateValidator.java @@ -1,113 +1,113 @@ -package ee.sk.smartid.common.certificate; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Validator for Smart-ID authentication certificates. - *

- * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. - * * @see https://www.skidsolutions.eu/resources/profiles/ - * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified and Non-Qualified and Digital authentication - */ -public final class SmartIdAuthenticationCertificateValidator { - - private static final Logger logger = LoggerFactory.getLogger(SmartIdAuthenticationCertificateValidator.class); - - private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; - private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; - private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; - - private SmartIdAuthenticationCertificateValidator() { - } - - /** - * Validates that the provided certificate can be used for authentication. - * - * @param certificate the certificate to validate - * @throws UnprocessableSmartIdResponseException if the certificate cannot be used for authentication - */ - public static void validate(X509Certificate certificate) { - if (!(isAfterApril2025Certificates(certificate) || isBeforeApril2025Certificates(certificate))) { - throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); - } - } - - // From April 2025 forward - // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 - // KeyUsage - digitalSignature - private static boolean isAfterApril2025Certificates(X509Certificate certificate) { - if (!hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { - return false; - } - boolean[] keyUsage = certificate.getKeyUsage(); - if (!(keyUsage != null && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE])) { - logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); - return false; - } - return true; - } - - // Before April 2025 - // Extended key usage - 1.3.6.1.5.5.7.3.2 - // Key Usage - digitalSignature, keyEncipherment, dataEncipherment - private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { - if (!hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { - return false; - } - boolean[] keyUsage = certificate.getKeyUsage(); - if (!(keyUsage != null - && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] - && keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] - && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { - logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); - return false; - } - return true; - } - - private static boolean hasExtendedKey(X509Certificate certificate, String oid) { - try { - List extendedKeyUsage = certificate.getExtendedKeyUsage(); - if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { - logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); - return false; - } - } catch (CertificateParsingException ex) { - throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); - } - return true; - } -} +package ee.sk.smartid.common.certificate; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Validator for Smart-ID authentication certificates. + *

+ * Values used for validation are based on Certificate and OCSP Profile for Smart-ID document. + * * @see https://www.skidsolutions.eu/resources/profiles/ + * * Chapter 2.2.2 Variable Extensions and section Smart-ID Qualified and Non-Qualified and Digital authentication + */ +public final class SmartIdAuthenticationCertificateValidator { + + private static final Logger logger = LoggerFactory.getLogger(SmartIdAuthenticationCertificateValidator.class); + + private static final int INDEX_OF_DIGITAL_SIGNATURE_VALUE = 0; + private static final int INDEX_OF_KEY_ENCIPHERMENT_VALUE = 2; + private static final int INDEX_OF_DATA_ENCIPHERMENT_VALUE = 3; + + private SmartIdAuthenticationCertificateValidator() { + } + + /** + * Validates that the provided certificate can be used for authentication. + * + * @param certificate the certificate to validate + * @throws UnprocessableSmartIdResponseException if the certificate cannot be used for authentication + */ + public static void validate(X509Certificate certificate) { + if (!(isAfterApril2025Certificates(certificate) || isBeforeApril2025Certificates(certificate))) { + throw new UnprocessableSmartIdResponseException("Provided certificate cannot be used for authentication"); + } + } + + // From April 2025 forward + // Extended key usage - 1.3.6.1.4.1.62306.5.7.0 + // KeyUsage - digitalSignature + private static boolean isAfterApril2025Certificates(X509Certificate certificate) { + if (!hasExtendedKey(certificate, "1.3.6.1.4.1.62306.5.7.0")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + // Before April 2025 + // Extended key usage - 1.3.6.1.5.5.7.3.2 + // Key Usage - digitalSignature, keyEncipherment, dataEncipherment + private static boolean isBeforeApril2025Certificates(X509Certificate certificate) { + if (!hasExtendedKey(certificate, "1.3.6.1.5.5.7.3.2")) { + return false; + } + boolean[] keyUsage = certificate.getKeyUsage(); + if (!(keyUsage != null + && keyUsage[INDEX_OF_DIGITAL_SIGNATURE_VALUE] + && keyUsage[INDEX_OF_KEY_ENCIPHERMENT_VALUE] + && keyUsage[INDEX_OF_DATA_ENCIPHERMENT_VALUE])) { + logger.debug("Certificate `{}` has invalid values for key usage.", certificate.getSubjectX500Principal()); + return false; + } + return true; + } + + private static boolean hasExtendedKey(X509Certificate certificate, String oid) { + try { + List extendedKeyUsage = certificate.getExtendedKeyUsage(); + if (extendedKeyUsage == null || extendedKeyUsage.stream().noneMatch(e -> e.equals(oid))) { + logger.debug("Certificate `{}` does not have extended key usage for authentication.", certificate.getSubjectX500Principal()); + return false; + } + } catch (CertificateParsingException ex) { + throw new UnprocessableSmartIdResponseException("Provided certificate for is incorrect and cannot be used for authentication", ex); + } + return true; + } +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java index 63916137..3fd6c9d3 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/CallbackUrl.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.net.URI; - -/** - * Represents a callback URL with an associated URL-safe token. - * - * @param initialCallbackUri the full callback URI including the token as a query parameter - * @param urlToken the generated URL-safe token - */ -public record CallbackUrl(URI initialCallbackUri, String urlToken) { -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.net.URI; + +/** + * Represents a callback URL with an associated URL-safe token. + * + * @param initialCallbackUri the full callback URI including the token as a query parameter + * @param urlToken the generated URL-safe token + */ +public record CallbackUrl(URI initialCallbackUri, String urlToken) { +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java index c700229d..4ff0cf07 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGenerator.java @@ -1,85 +1,85 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.SecureRandom; -import java.util.Base64; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Generates URL-safe tokens using a cryptographically secure random number generator. - */ -public class UrlSafeTokenGenerator { - - private static final int MIN_NR_OF_CHARACTERS = 22; - private static final int MAX_NR_OF_CHARACTERS = 86; - - private UrlSafeTokenGenerator() { - } - - /** - * Generates a random URL-safe token between 22 and 86 characters long. - * - * @return a random URL-safe token with random size between the specified lengths - */ - public static String random() { - return randomBetween(MIN_NR_OF_CHARACTERS, MAX_NR_OF_CHARACTERS); - } - - /** - * Generates a random URL-safe token of the specified length. - * - * @param length the length of the token to generate (must be between 22 and 86) - * @return a random URL-safe token of the specified length - */ - public static String ofLength(int length) { - return randomBetween(length, length); - } - - /** - * Generates a random URL-safe token between the specified minimum and maximum lengths. - * - * @param minLen the minimum length of the token (must be between 22 and 86) - * @param maxLen the maximum length of the token (must be between 22 and 86) - * @return a random URL-safe token with random size between the specified lengths - * @throws SmartIdClientException if the specified lengths are out of bounds or invalid - */ - public static String randomBetween(int minLen, int maxLen) { - if (minLen < MIN_NR_OF_CHARACTERS || maxLen > MAX_NR_OF_CHARACTERS || minLen > maxLen) { - throw new SmartIdClientException("Length must be between 22 and 86 chars"); - } - SecureRandom secureRandom = new SecureRandom(); - // Random length between minLen and maxLen (inclusive) - int targetLen = secureRandom.nextInt(maxLen - minLen + 1) + minLen; - byte[] bytes = new byte[64]; - secureRandom.nextBytes(bytes); - String random = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); - // Trim down to desired length - return random.substring(0, targetLen); - } -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.SecureRandom; +import java.util.Base64; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Generates URL-safe tokens using a cryptographically secure random number generator. + */ +public class UrlSafeTokenGenerator { + + private static final int MIN_NR_OF_CHARACTERS = 22; + private static final int MAX_NR_OF_CHARACTERS = 86; + + private UrlSafeTokenGenerator() { + } + + /** + * Generates a random URL-safe token between 22 and 86 characters long. + * + * @return a random URL-safe token with random size between the specified lengths + */ + public static String random() { + return randomBetween(MIN_NR_OF_CHARACTERS, MAX_NR_OF_CHARACTERS); + } + + /** + * Generates a random URL-safe token of the specified length. + * + * @param length the length of the token to generate (must be between 22 and 86) + * @return a random URL-safe token of the specified length + */ + public static String ofLength(int length) { + return randomBetween(length, length); + } + + /** + * Generates a random URL-safe token between the specified minimum and maximum lengths. + * + * @param minLen the minimum length of the token (must be between 22 and 86) + * @param maxLen the maximum length of the token (must be between 22 and 86) + * @return a random URL-safe token with random size between the specified lengths + * @throws SmartIdClientException if the specified lengths are out of bounds or invalid + */ + public static String randomBetween(int minLen, int maxLen) { + if (minLen < MIN_NR_OF_CHARACTERS || maxLen > MAX_NR_OF_CHARACTERS || minLen > maxLen) { + throw new SmartIdClientException("Length must be between 22 and 86 chars"); + } + SecureRandom secureRandom = new SecureRandom(); + // Random length between minLen and maxLen (inclusive) + int targetLen = secureRandom.nextInt(maxLen - minLen + 1) + minLen; + byte[] bytes = new byte[64]; + secureRandom.nextBytes(bytes); + String random = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + // Trim down to desired length + return random.substring(0, targetLen); + } +} diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java index 55db8d75..29803eee 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteraction.java @@ -1,87 +1,87 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionValidator; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * Interaction to be used in device-link based authentication and signing requests - * - * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ -public record DeviceLinkInteraction(DeviceLinkInteractionType type, - String displayText60, - String displayText200) implements SmartIdInteraction { - - /** - * Creates a new instance of {@link DeviceLinkInteraction}. - *

- * Display text fields will be validated based on interaction type. - * - * @param type interaction type (see {@link DeviceLinkInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ - public DeviceLinkInteraction { - if (type == null) { - throw new SmartIdRequestSetupException("Value for 'type' must be set"); - } - if (type == DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN) { - InteractionValidator.validate(type, displayText60); - } - if (type == DeviceLinkInteractionType.CONFIRMATION_MESSAGE) { - InteractionValidator.validate(type, displayText200); - } - } - - /** - * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#DISPLAY_TEXT_AND_PIN} - * - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @return instance of DeviceLinkInteraction - * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type - */ - public static DeviceLinkInteraction displayTextAndPin(String displayText60) { - return new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); - } - - /** - * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#CONFIRMATION_MESSAGE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return instance of DeviceLinkInteraction - * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type - */ - public static DeviceLinkInteraction confirmationMessage(String displayText200) { - return new DeviceLinkInteraction(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, null, displayText200); - } -} - +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * Interaction to be used in device-link based authentication and signing requests + * + * @param type the interactions type that can be used for device-link based flows (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record DeviceLinkInteraction(DeviceLinkInteractionType type, + String displayText60, + String displayText200) implements SmartIdInteraction { + + /** + * Creates a new instance of {@link DeviceLinkInteraction}. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type interaction type (see {@link DeviceLinkInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ + public DeviceLinkInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == DeviceLinkInteractionType.CONFIRMATION_MESSAGE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction displayTextAndPin(String displayText60) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link DeviceLinkInteraction} of type {@link DeviceLinkInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return instance of DeviceLinkInteraction + * @throws SmartIdRequestSetupException if text length exceeds max length of interaction type + */ + public static DeviceLinkInteraction confirmationMessage(String displayText200) { + return new DeviceLinkInteraction(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } +} + diff --git a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java index 1b33345c..2c1c1bda 100644 --- a/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionType.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionType; - -/** - * Interaction types that can be used in device link-based authentication and signing requests - */ -public enum DeviceLinkInteractionType implements InteractionType { - - /** - * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. - */ - DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. - */ - CONFIRMATION_MESSAGE("confirmationMessage", 200); - - private final String code; - private final int maxLength; - - DeviceLinkInteractionType(String code, int maxLength) { - this.code = code; - this.maxLength = maxLength; - } - - @Override - public String getCode() { - return code; - } - - @Override - public int getMaxLength() { - return maxLength; - } -} +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionType; + +/** + * Interaction types that can be used in device link-based authentication and signing requests + */ +public enum DeviceLinkInteractionType implements InteractionType { + + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ + CONFIRMATION_MESSAGE("confirmationMessage", 200); + + private final String code; + private final int maxLength; + + DeviceLinkInteractionType(String code, int maxLength) { + this.code = code; + this.maxLength = maxLength; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMaxLength() { + return maxLength; + } +} diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java index 9db4daa6..7981f9fb 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteraction.java @@ -1,98 +1,98 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import ee.sk.smartid.common.InteractionValidator; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -/** - * Interaction to be used in notification-based authentication and signing requests - * - * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - */ -public record NotificationInteraction(NotificationInteractionType type, - String displayText60, - String displayText200) implements Serializable, SmartIdInteraction { - - /** - * Constructs a new NotificationInteraction instance. - *

- * Display text fields will be validated based on interaction type. - * - * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @throws SmartIdRequestSetupException if display text fields have incorrect value based on the type - */ - public NotificationInteraction { - if (type == null) { - throw new SmartIdRequestSetupException("Value for 'type' must be set"); - } - if (type == NotificationInteractionType.DISPLAY_TEXT_AND_PIN) { - InteractionValidator.validate(type, displayText60); - } - if (type == NotificationInteractionType.CONFIRMATION_MESSAGE - || type == NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { - InteractionValidator.validate(type, displayText200); - } - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#DISPLAY_TEXT_AND_PIN} - * - * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). - * @return the interaction - */ - public static NotificationInteraction displayTextAndPin(String displayText60) { - return new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return the interaction - */ - public static NotificationInteraction confirmationMessage(String displayText200) { - return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE, null, displayText200); - } - - /** - * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE} - * - * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). - * @return the interaction - */ - public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { - return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, null, displayText200); - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import ee.sk.smartid.common.InteractionValidator; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +/** + * Interaction to be used in notification-based authentication and signing requests + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + */ +public record NotificationInteraction(NotificationInteractionType type, + String displayText60, + String displayText200) implements Serializable, SmartIdInteraction { + + /** + * Constructs a new NotificationInteraction instance. + *

+ * Display text fields will be validated based on interaction type. + * + * @param type the interactions type that can be used for notification based flows (see {@link NotificationInteractionType} for possible values) + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @throws SmartIdRequestSetupException if display text fields have incorrect value based on the type + */ + public NotificationInteraction { + if (type == null) { + throw new SmartIdRequestSetupException("Value for 'type' must be set"); + } + if (type == NotificationInteractionType.DISPLAY_TEXT_AND_PIN) { + InteractionValidator.validate(type, displayText60); + } + if (type == NotificationInteractionType.CONFIRMATION_MESSAGE + || type == NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE) { + InteractionValidator.validate(type, displayText200); + } + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#DISPLAY_TEXT_AND_PIN} + * + * @param displayText60 the text to be displayed on the device screen (maximum length 60 characters). + * @return the interaction + */ + public static NotificationInteraction displayTextAndPin(String displayText60) { + return new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText60, null); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessage(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE, null, displayText200); + } + + /** + * Creates a {@link NotificationInteraction} of type {@link NotificationInteractionType#CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE} + * + * @param displayText200 the text to be displayed on the device screen (maximum length 200 characters). + * @return the interaction + */ + public static NotificationInteraction confirmationMessageAndVerificationCodeChoice(String displayText200) { + return new NotificationInteraction(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, null, displayText200); + } +} diff --git a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java index 3ae5ec32..80f88363 100644 --- a/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java +++ b/src/main/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionType.java @@ -1,66 +1,66 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.common.InteractionType; - -/** - * Interaction types that can be used in notification-based authentication and signing requests - */ -public enum NotificationInteractionType implements InteractionType { - - /** - * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. - */ - DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. - */ - CONFIRMATION_MESSAGE("confirmationMessage", 200), - /** - * Provided text with max length of 200 chars will be shown on the device with confirmation dialog and verification code choice before entering the PIN. - */ - CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); - - private final String code; - private final int maxLength; - - NotificationInteractionType(String code, int maxLength) { - this.code = code; - this.maxLength = maxLength; - } - - @Override - public String getCode() { - return code; - } - - @Override - public int getMaxLength() { - return maxLength; - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.common.InteractionType; + +/** + * Interaction types that can be used in notification-based authentication and signing requests + */ +public enum NotificationInteractionType implements InteractionType { + + /** + * Provided text with max length of 60 chars will be displayed on the device with option to enter the PIN. + */ + DISPLAY_TEXT_AND_PIN("displayTextAndPIN", 60), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog before entering the PIN. + */ + CONFIRMATION_MESSAGE("confirmationMessage", 200), + /** + * Provided text with max length of 200 chars will be shown on the device with confirmation dialog and verification code choice before entering the PIN. + */ + CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE("confirmationMessageAndVerificationCodeChoice", 200); + + private final String code; + private final int maxLength; + + NotificationInteractionType(String code, int maxLength) { + this.code = code; + this.maxLength = maxLength; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMaxLength() { + return maxLength; + } +} diff --git a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java index 376f81cd..fb625ec6 100644 --- a/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/EnduringSmartIdException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exceptions that subclass this mark situations where something is wrong with - * client-side integration or how Relying Party account has been configured by Smart-ID operator - * or Smart-ID server is under maintenance. - * With these types of errors there is not recommended to ask the user for immediate retry. - */ -public abstract class EnduringSmartIdException extends SmartIdException { - - /** - * Constructs the exception with the specified message. - * - * @param message the message to describe the reason for the exception - */ - public EnduringSmartIdException(String message) { - super(message); - } - - /** - * Constructs a new exception with the specified message and cause. - * - * @param message the message to describe the reason for the exception - * @param cause the underlying cause of the exception - */ - public EnduringSmartIdException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exceptions that subclass this mark situations where something is wrong with + * client-side integration or how Relying Party account has been configured by Smart-ID operator + * or Smart-ID server is under maintenance. + * With these types of errors there is not recommended to ask the user for immediate retry. + */ +public abstract class EnduringSmartIdException extends SmartIdException { + + /** + * Constructs the exception with the specified message. + * + * @param message the message to describe the reason for the exception + */ + public EnduringSmartIdException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message the message to describe the reason for the exception + * @param cause the underlying cause of the exception + */ + public EnduringSmartIdException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java index 7329a81d..e126e25e 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionNotFoundException.java @@ -1,33 +1,33 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session with the given session ID could not be found. - */ -public class SessionNotFoundException extends SmartIdException { -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session with the given session ID could not be found. + */ +public class SessionNotFoundException extends SmartIdException { +} diff --git a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java index 6686e884..5736ac83 100644 --- a/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/SessionSecretMismatchException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when the session secret digest from the callback does not match the calculated digest. - */ -public class SessionSecretMismatchException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SessionSecretMismatchException(String message) { - super(message); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when the session secret digest from the callback does not match the calculated digest. + */ +public class SessionSecretMismatchException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SessionSecretMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/SmartIdException.java b/src/main/java/ee/sk/smartid/exception/SmartIdException.java index ade03ce5..b6fd4b2f 100644 --- a/src/main/java/ee/sk/smartid/exception/SmartIdException.java +++ b/src/main/java/ee/sk/smartid/exception/SmartIdException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * All Smart-ID exceptions subclass from this. - */ -public abstract class SmartIdException extends RuntimeException { - - public SmartIdException() { - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the underlying cause of this exception. - */ - public SmartIdException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * All Smart-ID exceptions subclass from this. + */ +public abstract class SmartIdException extends RuntimeException { + + public SmartIdException() { + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the underlying cause of this exception. + */ + public SmartIdException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java index 42bd0029..57c4193a 100644 --- a/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java +++ b/src/main/java/ee/sk/smartid/exception/UnprocessableSmartIdResponseException.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Thrown when validation of any Smart-ID API responses fail. - * This includes responses for session initialization requests and session status responses. - */ -public class UnprocessableSmartIdResponseException extends SmartIdClientException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UnprocessableSmartIdResponseException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param exception the exception that caused this exception to be thrown. - */ - public UnprocessableSmartIdResponseException(String message, Exception exception) { - super(message, exception); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Thrown when validation of any Smart-ID API responses fail. + * This includes responses for session initialization requests and session status responses. + */ +public class UnprocessableSmartIdResponseException extends SmartIdClientException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UnprocessableSmartIdResponseException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param exception the exception that caused this exception to be thrown. + */ + public UnprocessableSmartIdResponseException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/UserAccountException.java b/src/main/java/ee/sk/smartid/exception/UserAccountException.java index 8b1a5560..d964f758 100644 --- a/src/main/java/ee/sk/smartid/exception/UserAccountException.java +++ b/src/main/java/ee/sk/smartid/exception/UserAccountException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Subclasses of this exception are situation where something is wrong with user's Smart-ID account (or app) configuration. - * General practise is to display a notification and ask user to log in to Smart-ID self-service portal. - */ -public abstract class UserAccountException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserAccountException(String message) { - super(message); - } - -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Subclasses of this exception are situation where something is wrong with user's Smart-ID account (or app) configuration. + * General practise is to display a notification and ask user to log in to Smart-ID self-service portal. + */ +public abstract class UserAccountException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserAccountException(String message) { + super(message); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/UserActionException.java b/src/main/java/ee/sk/smartid/exception/UserActionException.java index 6ac7e5eb..56cf6cd0 100644 --- a/src/main/java/ee/sk/smartid/exception/UserActionException.java +++ b/src/main/java/ee/sk/smartid/exception/UserActionException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Subclasses of this exception are situation where user's action triggered ending session. - * General practise is to ask the user to try again. - */ -public abstract class UserActionException extends SmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserActionException(String message) { - super(message); - } -} +package ee.sk.smartid.exception; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Subclasses of this exception are situation where user's action triggered ending session. + * General practise is to ask the user to try again. + */ +public abstract class UserActionException extends SmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserActionException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java index 81958825..00372c2f 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ExpectedLinkedSessionException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Linked signature flow consists of two sessions - device link-based certificate choice session followed by the linked signature session. - * Exception will be thrown when linked signature session is not received after the device link-based certificate choice session, - * but some other session with the same document number is received instead. - */ -public class ExpectedLinkedSessionException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ExpectedLinkedSessionException() { - super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Linked signature flow consists of two sessions - device link-based certificate choice session followed by the linked signature session. + * Exception will be thrown when linked signature session is not received after the device link-based certificate choice session, + * but some other session with the same document number is received instead. + */ +public class ExpectedLinkedSessionException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ExpectedLinkedSessionException() { + super("The app received a different transaction while waiting for the linked session that follows the device-link based cert-choice session"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java index 645c52a7..bb262cd7 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ProtocolFailureException.java @@ -1,44 +1,44 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Exception thrown when the session status end result is PROTOCOL_FAILURE, indicating logical error in the signing protocol. - *

- * F.e. Constructed device link that user can interact with contains invalid schema. - */ -public class ProtocolFailureException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ProtocolFailureException() { - super("A logical error occurred in the signing protocol."); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Exception thrown when the session status end result is PROTOCOL_FAILURE, indicating logical error in the signing protocol. + *

+ * F.e. Constructed device link that user can interact with contains invalid schema. + */ +public class ProtocolFailureException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ProtocolFailureException() { + super("A logical error occurred in the signing protocol."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java index 760f143e..d618d1f9 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/RelyingPartyAccountConfigurationException.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exception will be thrown when there are problems with relying party account and access configuration - * or when relying party does not have access to the requested service. - *

- * F.e. Request is made with relying party UUID and incorrect relying party name. - */ -public class RelyingPartyAccountConfigurationException extends SmartIdClientException { - - /** - * Constructs the exception with message and cause. - * - * @param message the exception message - * @param exception underlying cause for this exception - */ - public RelyingPartyAccountConfigurationException(String message, Exception exception) { - super(message, exception); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exception will be thrown when there are problems with relying party account and access configuration + * or when relying party does not have access to the requested service. + *

+ * F.e. Request is made with relying party UUID and incorrect relying party name. + */ +public class RelyingPartyAccountConfigurationException extends SmartIdClientException { + + /** + * Constructs the exception with message and cause. + * + * @param message the exception message + * @param exception underlying cause for this exception + */ + public RelyingPartyAccountConfigurationException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java index 16f1708d..fc46fbd6 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/ServerMaintenanceException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Thrown when request cannot be process because the Smart-ID API server is under maintenance. - */ -public class ServerMaintenanceException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public ServerMaintenanceException() { - super("Server is under maintenance, retry later."); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Thrown when request cannot be process because the Smart-ID API server is under maintenance. + */ +public class ServerMaintenanceException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public ServerMaintenanceException() { + super("Server is under maintenance, retry later."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java index b20bf311..e40589e8 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdClientException.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * This exception is thrown if client (this library) has configuration errors. - */ -public class SmartIdClientException extends EnduringSmartIdException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdClientException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the exception that caused this exception to be thrown. - */ - public SmartIdClientException(String message, Throwable cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * This exception is thrown if client (this library) has configuration errors. + */ +public class SmartIdClientException extends EnduringSmartIdException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdClientException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ + public SmartIdClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java index 4333c0b2..3cf49946 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdRequestSetupException.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Exception thrown when there is an issue setting up a Smart-ID request. - * This could be due to invalid parameters, configuration issues, or other - * problems that prevent from successfully preparing the request. - */ -public class SmartIdRequestSetupException extends SmartIdClientException { - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public SmartIdRequestSetupException(String message) { - super(message); - } - - /** - * Constructs the exception with the specified exception message and cause. - * - * @param message the exception message. - * @param cause the exception that caused this exception to be thrown. - */ - public SmartIdRequestSetupException(String message, Exception cause) { - super(message, cause); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Exception thrown when there is an issue setting up a Smart-ID request. + * This could be due to invalid parameters, configuration issues, or other + * problems that prevent from successfully preparing the request. + */ +public class SmartIdRequestSetupException extends SmartIdClientException { + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public SmartIdRequestSetupException(String message) { + super(message); + } + + /** + * Constructs the exception with the specified exception message and cause. + * + * @param message the exception message. + * @param cause the exception that caused this exception to be thrown. + */ + public SmartIdRequestSetupException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java index d082abdc..b0d84fe5 100644 --- a/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java +++ b/src/main/java/ee/sk/smartid/exception/permanent/SmartIdServerException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.permanent; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.EnduringSmartIdException; - -/** - * Thrown when session status end result is SERVER_ERROR, indicating a server-side technical error. - */ -public class SmartIdServerException extends EnduringSmartIdException { - - /** - * Constructs the exception with default message. - */ - public SmartIdServerException() { - super("Process was terminated due to server-side technical error"); - } -} +package ee.sk.smartid.exception.permanent; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.EnduringSmartIdException; + +/** + * Thrown when session status end result is SERVER_ERROR, indicating a server-side technical error. + */ +public class SmartIdServerException extends EnduringSmartIdException { + + /** + * Constructs the exception with default message. + */ + public SmartIdServerException() { + super("Process was terminated due to server-side technical error"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java index f00d8ff6..4db92fb2 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/CertificateLevelMismatchException.java @@ -1,51 +1,51 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when returned certificate level is lower than the requested certificate level. - */ -public class CertificateLevelMismatchException extends UserAccountException { - - /** - * Constructs the exception with the default message. - */ - public CertificateLevelMismatchException() { - super("Signer's certificate is below requested certificate level"); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message - */ - public CertificateLevelMismatchException(String message) { - super(message); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when returned certificate level is lower than the requested certificate level. + */ +public class CertificateLevelMismatchException extends UserAccountException { + + /** + * Constructs the exception with the default message. + */ + public CertificateLevelMismatchException() { + super("Signer's certificate is below requested certificate level"); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message + */ + public CertificateLevelMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java index c3bb3616..2df63e82 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/DocumentUnusableException.java @@ -1,40 +1,40 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is DOCUMENT_UNUSABLE. - */ -public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { - - /** - * Constructs the exception with default message. - */ - public DocumentUnusableException() { - super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is DOCUMENT_UNUSABLE. + */ +public class DocumentUnusableException extends PersonShouldViewSmartIdPortalException { + + /** + * Constructs the exception with default message. + */ + public DocumentUnusableException() { + super("Document is unusable. User must either check his/her Smart-ID mobile application or turn to customer support for getting the exact reason."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java index c4ed6a99..86a1d0e6 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/NoSuitableAccountOfRequestedTypeFoundException.java @@ -1,46 +1,46 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when user does not have a suitable account for the requested operation. - *

- * F.e. user has non-qualified account with ADVANCED certificate level, - * but QUALIFIED certificate level is required for the operation. - */ -public class NoSuitableAccountOfRequestedTypeFoundException extends UserAccountException { - - /** - * Constructs the exception with default message. - */ - public NoSuitableAccountOfRequestedTypeFoundException() { - super("No suitable account of requested type found, but user has some other accounts."); - } - -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when user does not have a suitable account for the requested operation. + *

+ * F.e. user has non-qualified account with ADVANCED certificate level, + * but QUALIFIED certificate level is required for the operation. + */ +public class NoSuitableAccountOfRequestedTypeFoundException extends UserAccountException { + + /** + * Constructs the exception with default message. + */ + public NoSuitableAccountOfRequestedTypeFoundException() { + super("No suitable account of requested type found, but user has some other accounts."); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java index 98b06b2d..1a3b0b87 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/PersonShouldViewSmartIdPortalException.java @@ -1,51 +1,51 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when Smart-ID API indicates that there is an issue with user document and user should check its state. - */ -public class PersonShouldViewSmartIdPortalException extends UserAccountException { - - /** - * Constructs the exception with default message. - */ - public PersonShouldViewSmartIdPortalException() { - super("Person should view Smart-ID app or Smart-ID self-service portal now."); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message exception message - */ - public PersonShouldViewSmartIdPortalException(String message) { - super(message); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when Smart-ID API indicates that there is an issue with user document and user should check its state. + */ +public class PersonShouldViewSmartIdPortalException extends UserAccountException { + + /** + * Constructs the exception with default message. + */ + public PersonShouldViewSmartIdPortalException() { + super("Person should view Smart-ID app or Smart-ID self-service portal now."); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message exception message + */ + public PersonShouldViewSmartIdPortalException(String message) { + super(message); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java index 16470be2..c2b9a14c 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/RequiredInteractionNotSupportedByAppException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when the user's app version does not support any of the provided interactions. - */ -public class RequiredInteractionNotSupportedByAppException extends UserAccountException { - - /** - * Constructs the exception with the default message. - */ - public RequiredInteractionNotSupportedByAppException() { - super("User app version does not support any of the provided interactions."); - } - -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when the user's app version does not support any of the provided interactions. + */ +public class RequiredInteractionNotSupportedByAppException extends UserAccountException { + + /** + * Constructs the exception with the default message. + */ + public RequiredInteractionNotSupportedByAppException() { + super("User app version does not support any of the provided interactions."); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java index 74435578..725af6ec 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountNotFoundException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when user account does not exist with the given identifier or document number. - */ -public class UserAccountNotFoundException extends UserAccountException { - - /** - * Constructs the exception with message. - */ - public UserAccountNotFoundException() { - super("User account not found"); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when user account does not exist with the given identifier or document number. + */ +public class UserAccountNotFoundException extends UserAccountException { + + /** + * Constructs the exception with message. + */ + public UserAccountNotFoundException() { + super("User account not found"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java index d33f8e77..62e99b77 100644 --- a/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java +++ b/src/main/java/ee/sk/smartid/exception/useraccount/UserAccountUnusableException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraccount; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserAccountException; - -/** - * Thrown when session status end result is ACCOUNT_UNUSABLE. - */ -public class UserAccountUnusableException extends UserAccountException { - - /** - * Constructs the exception with the default exception message. - */ - public UserAccountUnusableException() { - super("The account is currently unusable"); - } -} +package ee.sk.smartid.exception.useraccount; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserAccountException; + +/** + * Thrown when session status end result is ACCOUNT_UNUSABLE. + */ +public class UserAccountUnusableException extends UserAccountException { + + /** + * Constructs the exception with the default exception message. + */ + public UserAccountUnusableException() { + super("The account is currently unusable"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java index cdfe920d..ce86589b 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/SessionTimeoutException.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status end result is TIMEOUT. - */ -public class SessionTimeoutException extends UserActionException { - - /** - * Constructs the exception with default message. - */ - public SessionTimeoutException() { - super("Session timed out without getting any response from user"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status end result is TIMEOUT. + */ +public class SessionTimeoutException extends UserActionException { + + /** + * Constructs the exception with default message. + */ + public SessionTimeoutException() { + super("Session timed out without getting any response from user"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java index 77912b04..edea95a6 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedCertChoiceException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_CERT_CHOICE. - * This happens when user has multiple accounts and presses Cancel on device choice screen on any device. - */ -public class UserRefusedCertChoiceException extends UserRefusedException { - - /** - * Constructs a new UserRefusedDisplayTextAndPinException with the default exception message. - */ - public UserRefusedCertChoiceException() { - super("User has multiple accounts and pressed Cancel on device choice screen on any device."); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_CERT_CHOICE. + * This happens when user has multiple accounts and presses Cancel on device choice screen on any device. + */ +public class UserRefusedCertChoiceException extends UserRefusedException { + + /** + * Constructs a new UserRefusedDisplayTextAndPinException with the default exception message. + */ + public UserRefusedCertChoiceException() { + super("User has multiple accounts and pressed Cancel on device choice screen on any device."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java index 13178fb2..8444d5c3 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on confirmation message screen. - */ -public class UserRefusedConfirmationMessageException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedConfirmationMessageException() { - super("User cancelled on confirmationMessage screen"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation message screen. + */ +public class UserRefusedConfirmationMessageException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedConfirmationMessageException() { + super("User cancelled on confirmationMessage screen"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java index aee41245..06a3c5a4 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedConfirmationMessageWithVerificationChoiceException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on confirmation and verification code choice screen. - */ -public class UserRefusedConfirmationMessageWithVerificationChoiceException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedConfirmationMessageWithVerificationChoiceException() { - super("User cancelled on confirmationMessageAndVerificationCodeChoice screen"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on confirmation and verification code choice screen. + */ +public class UserRefusedConfirmationMessageWithVerificationChoiceException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedConfirmationMessageWithVerificationChoiceException() { + super("User cancelled on confirmationMessageAndVerificationCodeChoice screen"); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java index 67b3ac8a..723d0a18 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedDisplayTextAndPinException.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Thrown when session status end result is USER_REFUSED_INTERACTION. - * This happens when user presses Cancel on display text and PIN screen. - */ -public class UserRefusedDisplayTextAndPinException extends UserRefusedException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedDisplayTextAndPinException() { - super("User pressed Cancel on PIN screen."); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Thrown when session status end result is USER_REFUSED_INTERACTION. + * This happens when user presses Cancel on display text and PIN screen. + */ +public class UserRefusedDisplayTextAndPinException extends UserRefusedException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedDisplayTextAndPinException() { + super("User pressed Cancel on PIN screen."); + } +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java index 7492111a..d729292d 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserRefusedException.java @@ -1,52 +1,52 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status end result is USER_REFUSED. - */ -public class UserRefusedException extends UserActionException { - - /** - * Constructs the exception with the default exception message. - */ - public UserRefusedException() { - super("User pressed cancel in app"); - } - - /** - * Constructs the exception with the specified exception message. - * - * @param message the exception message. - */ - public UserRefusedException(String message) { - super(message); - } - -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status end result is USER_REFUSED. + */ +public class UserRefusedException extends UserActionException { + + /** + * Constructs the exception with the default exception message. + */ + public UserRefusedException() { + super("User pressed cancel in app"); + } + + /** + * Constructs the exception with the specified exception message. + * + * @param message the exception message. + */ + public UserRefusedException(String message) { + super(message); + } + +} diff --git a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java index 380e794e..24b4256b 100644 --- a/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java +++ b/src/main/java/ee/sk/smartid/exception/useraction/UserSelectedWrongVerificationCodeException.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.exception.useraction; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import ee.sk.smartid.exception.UserActionException; - -/** - * Thrown when session status result is WRONG_VC. - * This happens when user selects wrong verification code in the app. - */ -public class UserSelectedWrongVerificationCodeException extends UserActionException { - - /** - * Constructs the exception with the default exception message. - */ - public UserSelectedWrongVerificationCodeException() { - super("User selected wrong verification code"); - } -} +package ee.sk.smartid.exception.useraction; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import ee.sk.smartid.exception.UserActionException; + +/** + * Thrown when session status result is WRONG_VC. + * This happens when user selects wrong verification code in the app. + */ +public class UserSelectedWrongVerificationCodeException extends UserActionException { + + /** + * Constructs the exception with the default exception message. + */ + public UserSelectedWrongVerificationCodeException() { + super("User selected wrong verification code"); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java index a0ac91a8..a19b587c 100644 --- a/src/main/java/ee/sk/smartid/rest/LoggingFilter.java +++ b/src/main/java/ee/sk/smartid/rest/LoggingFilter.java @@ -1,158 +1,158 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.nio.charset.Charset; - -import org.glassfish.jersey.message.MessageUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; -import jakarta.ws.rs.client.ClientResponseContext; -import jakarta.ws.rs.client.ClientResponseFilter; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.WriterInterceptor; -import jakarta.ws.rs.ext.WriterInterceptorContext; - -public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor { - - private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); - private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; - - @Override - public void filter(ClientRequestContext requestContext) { - if (logger.isDebugEnabled()) { - logUrl(requestContext); - } - if (logger.isTraceEnabled()) { - logHeaders(requestContext); - if (requestContext.hasEntity()) { - wrapEntityStreamWithLogger(requestContext); - } - } - } - - @Override - public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { - if (logger.isDebugEnabled()) { - logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); - } - if (logger.isTraceEnabled() && responseContext.hasEntity()) { - logResponseBody(responseContext); - } - } - - @Override - public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - context.proceed(); - if (logger.isTraceEnabled()) { - logRequestBody(context); - } - } - - private void logUrl(ClientRequestContext requestContext) { - String method = requestContext.getMethod(); - URI uri = requestContext.getUri(); - logger.debug(method + " " + uri.toString()); - } - - private void logHeaders(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getStringHeaders(); - if (headers != null) { - logger.trace("Request headers: {}", headers); - } - } - - private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { - OutputStream entityStream = requestContext.getEntityStream(); - LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); - requestContext.setEntityStream(loggingOutputStream); - requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); - } - - private void logResponseBody(ClientResponseContext responseContext) throws IOException { - Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); - InputStream entityStream = responseContext.getEntityStream(); - byte[] bodyBytes = readInputStreamBytes(entityStream); - responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); - logger.trace("Response body: " + new String(bodyBytes, charset)); - } - - private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { - ByteArrayOutputStream result = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = entityStream.read(buffer)) != -1) { - result.write(buffer, 0, length); - } - return result.toByteArray(); - } - - private void logRequestBody(WriterInterceptorContext context) { - LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); - if (loggingOutputStream != null) { - Charset charset = MessageUtils.getCharset(context.getMediaType()); - byte[] bytes = loggingOutputStream.getBytes(); - logger.trace("Message body: " + new String(bytes, charset)); - } - } - - public static class LoggingOutputStream extends FilterOutputStream { - - private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - public LoggingOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(byte[] b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } - - @Override - public void write(int b) throws IOException { - super.write(b); - byteArrayOutputStream.write(b); - } - - public byte[] getBytes() { - return byteArrayOutputStream.toByteArray(); - } - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; + +import org.glassfish.jersey.message.MessageUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter, WriterInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + private static final String LOGGING_OUTPUT_STREAM_PROPERTY = "loggingOutputStream"; + + @Override + public void filter(ClientRequestContext requestContext) { + if (logger.isDebugEnabled()) { + logUrl(requestContext); + } + if (logger.isTraceEnabled()) { + logHeaders(requestContext); + if (requestContext.hasEntity()) { + wrapEntityStreamWithLogger(requestContext); + } + } + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Response status: " + responseContext.getStatus() + " - " + responseContext.getStatusInfo()); + } + if (logger.isTraceEnabled() && responseContext.hasEntity()) { + logResponseBody(responseContext); + } + } + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + context.proceed(); + if (logger.isTraceEnabled()) { + logRequestBody(context); + } + } + + private void logUrl(ClientRequestContext requestContext) { + String method = requestContext.getMethod(); + URI uri = requestContext.getUri(); + logger.debug(method + " " + uri.toString()); + } + + private void logHeaders(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getStringHeaders(); + if (headers != null) { + logger.trace("Request headers: {}", headers); + } + } + + private void wrapEntityStreamWithLogger(ClientRequestContext requestContext) { + OutputStream entityStream = requestContext.getEntityStream(); + LoggingOutputStream loggingOutputStream = new LoggingOutputStream(entityStream); + requestContext.setEntityStream(loggingOutputStream); + requestContext.setProperty(LOGGING_OUTPUT_STREAM_PROPERTY, loggingOutputStream); + } + + private void logResponseBody(ClientResponseContext responseContext) throws IOException { + Charset charset = MessageUtils.getCharset(responseContext.getMediaType()); + InputStream entityStream = responseContext.getEntityStream(); + byte[] bodyBytes = readInputStreamBytes(entityStream); + responseContext.setEntityStream(new ByteArrayInputStream(bodyBytes)); + logger.trace("Response body: " + new String(bodyBytes, charset)); + } + + private byte[] readInputStreamBytes(InputStream entityStream) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = entityStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toByteArray(); + } + + private void logRequestBody(WriterInterceptorContext context) { + LoggingOutputStream loggingOutputStream = (LoggingOutputStream) context.getProperty(LOGGING_OUTPUT_STREAM_PROPERTY); + if (loggingOutputStream != null) { + Charset charset = MessageUtils.getCharset(context.getMediaType()); + byte[] bytes = loggingOutputStream.getBytes(); + logger.trace("Message body: " + new String(bytes, charset)); + } + } + + public static class LoggingOutputStream extends FilterOutputStream { + + private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + public LoggingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } + + @Override + public void write(int b) throws IOException { + super.write(b); + byteArrayOutputStream.write(b); + } + + public byte[] getBytes() { + return byteArrayOutputStream.toByteArray(); + } + } +} diff --git a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java index a8994735..4a1c2fd3 100644 --- a/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java +++ b/src/main/java/ee/sk/smartid/rest/SessionStatusPoller.java @@ -1,109 +1,109 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * Provides methods for querying sessions status and polling session status - */ -public class SessionStatusPoller { - - private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); - - private final SmartIdConnector connector; - private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; - private long pollingSleepTimeout = 1L; - - /** - * Constructs a new SessionStatusPoller with the specified SmartIdConnector. - * - * @param connector the SmartIdConnector to use for querying session status. - */ - public SessionStatusPoller(SmartIdConnector connector) { - this.connector = connector; - } - - /** - * Loops session status query until state is COMPLETE - * - * @param sessionId session id from init session response - * @return Sessions status - */ - public SessionStatus fetchFinalSessionStatus(String sessionId) { - logger.debug("Starting to poll session status for session {}", sessionId); - try { - return pollForFinalSessionStatus(sessionId); - } catch (InterruptedException ex) { - logger.error("Failed to poll session status", ex); - throw new SmartIdClientException("Failed to poll session status", ex); - } - } - - private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { - SessionStatus sessionStatus = null; - while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { - sessionStatus = getSessionStatus(sessionId); - if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { - break; - } - logger.debug("Sleeping for {} {}", pollingSleepTimeout, pollingSleepTimeUnit); - pollingSleepTimeUnit.sleep(pollingSleepTimeout); - } - logger.debug("Got final session status response"); - return sessionStatus; - } - - /** - * Query session status - * - * @param sessionId session id from init session response - * @return Sessions status - */ - public SessionStatus getSessionStatus(String sessionId) { - logger.debug("Querying session status"); - return connector.getSessionStatus(sessionId); - } - - /** - * Set polling sleep time - * - * @param unit time unit {@link TimeUnit} - * @param timeout time - */ - public void setPollingSleepTime(TimeUnit unit, long timeout) { - logger.debug("Setting polling sleep time to {} {}", timeout, unit); - this.pollingSleepTimeUnit = unit; - this.pollingSleepTimeout = timeout; - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * Provides methods for querying sessions status and polling session status + */ +public class SessionStatusPoller { + + private static final Logger logger = LoggerFactory.getLogger(SessionStatusPoller.class); + + private final SmartIdConnector connector; + private TimeUnit pollingSleepTimeUnit = TimeUnit.SECONDS; + private long pollingSleepTimeout = 1L; + + /** + * Constructs a new SessionStatusPoller with the specified SmartIdConnector. + * + * @param connector the SmartIdConnector to use for querying session status. + */ + public SessionStatusPoller(SmartIdConnector connector) { + this.connector = connector; + } + + /** + * Loops session status query until state is COMPLETE + * + * @param sessionId session id from init session response + * @return Sessions status + */ + public SessionStatus fetchFinalSessionStatus(String sessionId) { + logger.debug("Starting to poll session status for session {}", sessionId); + try { + return pollForFinalSessionStatus(sessionId); + } catch (InterruptedException ex) { + logger.error("Failed to poll session status", ex); + throw new SmartIdClientException("Failed to poll session status", ex); + } + } + + private SessionStatus pollForFinalSessionStatus(String sessionId) throws InterruptedException { + SessionStatus sessionStatus = null; + while (sessionStatus == null || "RUNNING".equalsIgnoreCase(sessionStatus.getState())) { + sessionStatus = getSessionStatus(sessionId); + if (sessionStatus != null && "COMPLETE".equalsIgnoreCase(sessionStatus.getState())) { + break; + } + logger.debug("Sleeping for {} {}", pollingSleepTimeout, pollingSleepTimeUnit); + pollingSleepTimeUnit.sleep(pollingSleepTimeout); + } + logger.debug("Got final session status response"); + return sessionStatus; + } + + /** + * Query session status + * + * @param sessionId session id from init session response + * @return Sessions status + */ + public SessionStatus getSessionStatus(String sessionId) { + logger.debug("Querying session status"); + return connector.getSessionStatus(sessionId); + } + + /** + * Set polling sleep time + * + * @param unit time unit {@link TimeUnit} + * @param timeout time + */ + public void setPollingSleepTime(TimeUnit unit, long timeout) { + logger.debug("Setting polling sleep time to {} {}", timeout, unit); + this.pollingSleepTimeUnit = unit; + this.pollingSleepTimeout = timeout; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java index 54f3d60a..37ad21a8 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdConnector.java @@ -1,197 +1,197 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; - -/** - * SmartIdConnector is the main interface for interacting with the Smart-ID service. - * It provides methods to initiate various types of sessions (authentication, signature, certificate choice) - * and to query session status and certificates. - */ -public interface SmartIdConnector extends Serializable { - - /** - * Get session status for the given session ID. - * - * @param sessionId The session ID - * @return The session status - * @throws SessionNotFoundException If the session is not found - */ - SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; - - /** - * Set the session status response socket open time - * - * @param sessionStatusResponseSocketOpenTimeUnit The time unit of the open time - * @param sessionStatusResponseSocketOpenTimeValue The value of the open time - */ - void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); - - /** - * Initiates a device link based certificate choice request. - * - * @param request CertificateChoiceSessionRequest containing necessary parameters - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request); - - /** - * Initiates a linked notification based signature session. - * - * @param request LinkedSignatureSessionRequest containing necessary parameters - * @param documentNumber The document number to be used for the session - * @return LinkedSignatureSessionResponse containing sessionID - */ - LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); - - /** - * Initiates a notification based certificate choice request. - * - * @param request CertificateChoiceSessionRequest containing necessary parameters - * @param semanticsIdentifier The semantics identifier to be used for the session - * @return NotificationCertificateChoiceSessionResponse containing sessionID - */ - NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Queries signing certificate by document number. - * - * @param request CertificateByDocumentNumberRequest containing necessary parameters - * @param documentNumber The document number - * @return CertificateResponse containing response state and certificate information. - */ - CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); - - /** - * Initiates a device link based signature sessions. - * - * @param request SignatureSessionRequest containing necessary parameters for the signature session - * @param semanticsIdentifier The semantics identifier - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Initiates a device link based signature sessions. - * - * @param request SignatureSessionRequest containing necessary parameters for the signature session - * @param documentNumber The document number - * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. - */ - DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber); - - /** - * Initiates a notification-based signature session using a semantics identifier. - * - * @param request The notification signature session request containing the required parameters. - * @param semanticsIdentifier The semantics identifier for the user initiating the session. - * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. - */ - NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); - - /** - * Initiates a notification-based signature session using a document number. - * - * @param request The notification signature session request containing the required parameters. - * @param documentNumber The document number for the user initiating the session. - * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. - */ - NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber); - - /** - * Set the SSL context to use for secure communication - * - * @param sslContext The SSL context - */ - void setSslContext(SSLContext sslContext); - - /** - * Create anonymous authentication session with device link - * - * @param authenticationRequest The device link authentication session request - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest); - - /** - * Create authentication session with device link using semantics identifier - * - * @param authenticationRequest The device link authentication session request - * @param semanticsIdentifier The semantics identifier - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); - - /** - * Create authentication session with device link using document number - * - * @param authenticationRequest The device link authentication session request - * @param documentNumber The document number - * @return The device link authentication session response - */ - DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); - - /** - * Create authentication session with notification using semantics identifier - * - * @param authenticationRequest The notification authentication session request - * @param semanticsIdentifier The semantics identifier - * @return The notification authentication session response - */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); - - /** - * Create authentication session with notification using document number - * - * @param authenticationRequest The notification authentication session request - * @param documentNumber The document number - * @return The notification authentication session response - */ - NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber); -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; + +/** + * SmartIdConnector is the main interface for interacting with the Smart-ID service. + * It provides methods to initiate various types of sessions (authentication, signature, certificate choice) + * and to query session status and certificates. + */ +public interface SmartIdConnector extends Serializable { + + /** + * Get session status for the given session ID. + * + * @param sessionId The session ID + * @return The session status + * @throws SessionNotFoundException If the session is not found + */ + SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException; + + /** + * Set the session status response socket open time + * + * @param sessionStatusResponseSocketOpenTimeUnit The time unit of the open time + * @param sessionStatusResponseSocketOpenTimeValue The value of the open time + */ + void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue); + + /** + * Initiates a device link based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request); + + /** + * Initiates a linked notification based signature session. + * + * @param request LinkedSignatureSessionRequest containing necessary parameters + * @param documentNumber The document number to be used for the session + * @return LinkedSignatureSessionResponse containing sessionID + */ + LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber); + + /** + * Initiates a notification based certificate choice request. + * + * @param request CertificateChoiceSessionRequest containing necessary parameters + * @param semanticsIdentifier The semantics identifier to be used for the session + * @return NotificationCertificateChoiceSessionResponse containing sessionID + */ + NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Queries signing certificate by document number. + * + * @param request CertificateByDocumentNumberRequest containing necessary parameters + * @param documentNumber The document number + * @return CertificateResponse containing response state and certificate information. + */ + CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request); + + /** + * Initiates a device link based signature sessions. + * + * @param request SignatureSessionRequest containing necessary parameters for the signature session + * @param semanticsIdentifier The semantics identifier + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a device link based signature sessions. + * + * @param request SignatureSessionRequest containing necessary parameters for the signature session + * @param documentNumber The document number + * @return DeviceLinkSessionResponse containing sessionID, sessionToken, sessionSecret and deviceLinkBase URL. + */ + DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber); + + /** + * Initiates a notification-based signature session using a semantics identifier. + * + * @param request The notification signature session request containing the required parameters. + * @param semanticsIdentifier The semantics identifier for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier); + + /** + * Initiates a notification-based signature session using a document number. + * + * @param request The notification signature session request containing the required parameters. + * @param documentNumber The document number for the user initiating the session. + * @return NotificationSignatureSessionResponse containing the session ID and verification code (VC) information. + */ + NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber); + + /** + * Set the SSL context to use for secure communication + * + * @param sslContext The SSL context + */ + void setSslContext(SSLContext sslContext); + + /** + * Create anonymous authentication session with device link + * + * @param authenticationRequest The device link authentication session request + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest); + + /** + * Create authentication session with device link using semantics identifier + * + * @param authenticationRequest The device link authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with device link using document number + * + * @param authenticationRequest The device link authentication session request + * @param documentNumber The document number + * @return The device link authentication session response + */ + DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber); + + /** + * Create authentication session with notification using semantics identifier + * + * @param authenticationRequest The notification authentication session request + * @param semanticsIdentifier The semantics identifier + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier); + + /** + * Create authentication session with notification using document number + * + * @param authenticationRequest The notification authentication session request + * @param documentNumber The document number + * @return The notification authentication session response + */ + NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber); +} diff --git a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java index cf3e5bae..af11880e 100644 --- a/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java +++ b/src/main/java/ee/sk/smartid/rest/SmartIdRestConnector.java @@ -1,411 +1,411 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; - -import java.io.Serial; -import java.net.URI; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLContext; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SessionStatusRequest; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.ClientErrorException; -import jakarta.ws.rs.ForbiddenException; -import jakarta.ws.rs.NotAuthorizedException; -import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.ServerErrorException; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Smart-ID REST connector implementation. - */ -public class SmartIdRestConnector implements SmartIdConnector { - - @Serial - private static final long serialVersionUID = 2025_09_10L; - - private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); - - private static final String SESSION_STATUS_URI = "/session/{sessionId}"; - - private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; - private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; - - private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "signature/certificate-choice/notification/etsi"; - - private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; - - private static final String DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/device-link/etsi"; - private static final String DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/device-link/document"; - - private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; - private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; - - private static final String ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH = "authentication/device-link/anonymous"; - private static final String DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/device-link/etsi"; - private static final String DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/device-link/document"; - - private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; - private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; - - private final String endpointUrl; - private transient Configuration clientConfig; - private transient Client configuredClient; - private transient SSLContext sslContext; - private long sessionStatusResponseSocketOpenTimeValue; - private TimeUnit sessionStatusResponseSocketOpenTimeUnit; - - /** - * Creates a new instance of SmartIdRestConnector. - * - * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) - */ - public SmartIdRestConnector(String baseUrl) { - this.endpointUrl = baseUrl; - } - - /** - * Creates a new instance of SmartIdRestConnector with a pre-configured client. - * - * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) - * @param configuredClient a pre-configured client instace - */ - public SmartIdRestConnector(String baseUrl, Client configuredClient) { - this(baseUrl); - this.configuredClient = configuredClient; - } - - @Override - public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { - logger.debug("Getting session status for sessionId: {}", sessionId); - SessionStatusRequest request = createSessionStatusRequest(sessionId); - UriBuilder uriBuilder = UriBuilder - .fromUri(endpointUrl) - .path(SESSION_STATUS_URI); - addResponseSocketOpenTimeUrlParameter(request, uriBuilder); - URI uri = uriBuilder.build(sessionId); - - try { - return prepareClient(uri).get(SessionStatus.class); - } catch (NotFoundException ex) { - logger.warn("Session {} not found: {}", request, ex.getMessage()); - throw new SessionNotFoundException(); - } - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - logger.debug("Starting device link authentication session with semantics identifier"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { - logger.debug("Starting device link authentication session with document number"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest) { - logger.debug("Starting anonymous device link authentication session"); - URI uri = UriBuilder.fromUri(endpointUrl) - .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) - .build(); - return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); - } - - @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); - } - - @Override - public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request) { - logger.debug("Initiating device link based certificate choice request"); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber) { - logger.debug("Starting linked notification-based signature session"); - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, LinkedSignatureSessionResponse.class); - } - - @Override - public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); - } - - public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, CertificateResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, DeviceLinkSessionResponse.class); - } - - @Override - public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) - .path(semanticsIdentifier.getIdentifier()) - .build(); - return postRequest(uri, request, NotificationSignatureSessionResponse.class); - } - - @Override - public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber) { - URI uri = UriBuilder - .fromUri(endpointUrl) - .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) - .path(documentNumber) - .build(); - return postRequest(uri, request, NotificationSignatureSessionResponse.class); - } - - @Override - public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { - this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; - this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; - } - - @Override - public void setSslContext(SSLContext sslContext) { - this.sslContext = sslContext; - } - - /** - * Prepare client for the request. - * - * @param uri the target URI - * @return prepared invocation builder - */ - protected Invocation.Builder prepareClient(URI uri) { - Client client; - if (this.configuredClient == null) { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - if (null != this.clientConfig) { - clientBuilder.withConfig(this.clientConfig); - } - if (null != this.sslContext) { - clientBuilder.sslContext(this.sslContext); - } - client = clientBuilder.build(); - } else { - client = this.configuredClient; - } - - return client - .register(new LoggingFilter()) - .target(uri) - .request() - .accept(APPLICATION_JSON_TYPE) - .header("User-Agent", buildUserAgentString()); - } - - /** - * Build user-agent string. - * - * @return user-agent string in the format: smart-id-java-client/[client-version] (Java/[java-version]) - */ - protected String buildUserAgentString() { - return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; - } - - /** - * Get client version from package implementation version. - * - * @return client version or "-" - */ - protected String getClientVersion() { - String clientVersion = getClass().getPackage().getImplementationVersion(); - return clientVersion == null ? "-" : clientVersion; - } - - /** - * Get JDK major version. - * - * @return JDK major version or "-" - */ - protected String getJdkMajorVersion() { - try { - return System.getProperty("java.version").split("_")[0]; - } catch (Exception e) { - return "-"; - } - } - - private T postRequest(URI uri, V request, Class responseType) { - try { - Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); - return prepareClient(uri).post(requestEntity, responseType); - } catch (NotAuthorizedException ex) { - logger.warn("Request is unauthorized for URI {}", uri, ex); - throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, ex); - } catch (BadRequestException ex) { - logger.warn("Request is invalid for URI {}", uri, ex); - throw new SmartIdClientException("Server refused the request", ex); - } catch (NotFoundException e) { - logger.warn("User account not found for URI " + uri, e); - throw new UserAccountNotFoundException(); - } catch (ForbiddenException ex) { - logger.warn("No permission to issue the request", ex); - throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); - } catch (ClientErrorException ex) { - if (ex.getResponse().getStatus() == 471) { - logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); - throw new NoSuitableAccountOfRequestedTypeFoundException(); - } - if (ex.getResponse().getStatus() == 472) { - logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", ex); - throw new PersonShouldViewSmartIdPortalException(); - } - if (ex.getResponse().getStatus() == 480) { - logger.warn("Client-side API is too old and not supported anymore"); - throw new SmartIdClientException("Client-side API is too old and not supported anymore"); - } - throw ex; - } catch (ServerErrorException ex) { - if (ex.getResponse().getStatus() == 580) { - logger.warn("Server is under maintenance, retry later", ex); - throw new ServerMaintenanceException(); - } - throw ex; - } - } - - private SessionStatusRequest createSessionStatusRequest(String sessionId) { - var request = new SessionStatusRequest(sessionId); - if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { - request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); - } - return request; - } - - private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { - if (request.isResponseSocketOpenTimeSet()) { - TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); - long timeValue = request.getResponseSocketOpenTimeValue(); - long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); - uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); - } - } +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + +import java.io.Serial; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SessionStatusRequest; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Smart-ID REST connector implementation. + */ +public class SmartIdRestConnector implements SmartIdConnector { + + @Serial + private static final long serialVersionUID = 2025_09_10L; + + private static final Logger logger = LoggerFactory.getLogger(SmartIdRestConnector.class); + + private static final String SESSION_STATUS_URI = "/session/{sessionId}"; + + private static final String DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH = "signature/certificate-choice/device-link/anonymous"; + private static final String LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "signature/notification/linked"; + + private static final String NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH = "signature/certificate-choice/notification/etsi"; + + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/"; + + private static final String DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/device-link/etsi"; + private static final String DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/device-link/document"; + + private static final String NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH = "/signature/notification/etsi"; + private static final String NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document"; + + private static final String ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH = "authentication/device-link/anonymous"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/device-link/etsi"; + private static final String DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/device-link/document"; + + private static final String NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH = "authentication/notification/etsi"; + private static final String NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH = "authentication/notification/document"; + + private final String endpointUrl; + private transient Configuration clientConfig; + private transient Client configuredClient; + private transient SSLContext sslContext; + private long sessionStatusResponseSocketOpenTimeValue; + private TimeUnit sessionStatusResponseSocketOpenTimeUnit; + + /** + * Creates a new instance of SmartIdRestConnector. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + */ + public SmartIdRestConnector(String baseUrl) { + this.endpointUrl = baseUrl; + } + + /** + * Creates a new instance of SmartIdRestConnector with a pre-configured client. + * + * @param baseUrl The base URL of the Smart-ID API (e.g. https://sid.demo.sk.ee/smart-id-rp/v3/) + * @param configuredClient a pre-configured client instace + */ + public SmartIdRestConnector(String baseUrl, Client configuredClient) { + this(baseUrl); + this.configuredClient = configuredClient; + } + + @Override + public SessionStatus getSessionStatus(String sessionId) throws SessionNotFoundException { + logger.debug("Getting session status for sessionId: {}", sessionId); + SessionStatusRequest request = createSessionStatusRequest(sessionId); + UriBuilder uriBuilder = UriBuilder + .fromUri(endpointUrl) + .path(SESSION_STATUS_URI); + addResponseSocketOpenTimeUrlParameter(request, uriBuilder); + URI uri = uriBuilder.build(sessionId); + + try { + return prepareClient(uri).get(SessionStatus.class); + } catch (NotFoundException ex) { + logger.warn("Session {} not found: {}", request, ex.getMessage()); + throw new SessionNotFoundException(); + } + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + logger.debug("Starting device link authentication session with semantics identifier"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DEVICE_LINK_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest, String documentNumber) { + logger.debug("Starting device link authentication session with document number"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(DEVICE_LINK_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initAnonymousDeviceLinkAuthentication(DeviceLinkAuthenticationSessionRequest authenticationRequest) { + logger.debug("Starting anonymous device link authentication session"); + URI uri = UriBuilder.fromUri(endpointUrl) + .path(ANONYMOUS_DEVICE_LINK_AUTHENTICATION_PATH) + .build(); + return postRequest(uri, authenticationRequest, DeviceLinkSessionResponse.class); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); + } + + @Override + public NotificationAuthenticationSessionResponse initNotificationAuthentication(NotificationAuthenticationSessionRequest authenticationRequest, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_AUTHENTICATION_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, authenticationRequest, NotificationAuthenticationSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkCertificateChoice(DeviceLinkCertificateChoiceSessionRequest request) { + logger.debug("Initiating device link based certificate choice request"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_CERTIFICATE_CHOICE_DEVICE_LINK_PATH) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public LinkedSignatureSessionResponse initLinkedNotificationSignature(LinkedSignatureSessionRequest request, String documentNumber) { + logger.debug("Starting linked notification-based signature session"); + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(LINKED_NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, LinkedSignatureSessionResponse.class); + } + + @Override + public NotificationCertificateChoiceSessionResponse initNotificationCertificateChoice(NotificationCertificateChoiceSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_CERTIFICATE_CHOICE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, NotificationCertificateChoiceSessionResponse.class); + } + + public CertificateResponse getCertificateByDocumentNumber(String documentNumber, CertificateByDocumentNumberRequest request) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, CertificateResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public DeviceLinkSessionResponse initDeviceLinkSignature(DeviceLinkSignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(DEVICE_LINK_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, DeviceLinkSessionResponse.class); + } + + @Override + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, SemanticsIdentifier semanticsIdentifier) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_SEMANTIC_IDENTIFIER_PATH) + .path(semanticsIdentifier.getIdentifier()) + .build(); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); + } + + @Override + public NotificationSignatureSessionResponse initNotificationSignature(NotificationSignatureSessionRequest request, String documentNumber) { + URI uri = UriBuilder + .fromUri(endpointUrl) + .path(NOTIFICATION_SIGNATURE_WITH_DOCUMENT_NUMBER_PATH) + .path(documentNumber) + .build(); + return postRequest(uri, request, NotificationSignatureSessionResponse.class); + } + + @Override + public void setSessionStatusResponseSocketOpenTime(TimeUnit sessionStatusResponseSocketOpenTimeUnit, long sessionStatusResponseSocketOpenTimeValue) { + this.sessionStatusResponseSocketOpenTimeUnit = sessionStatusResponseSocketOpenTimeUnit; + this.sessionStatusResponseSocketOpenTimeValue = sessionStatusResponseSocketOpenTimeValue; + } + + @Override + public void setSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + } + + /** + * Prepare client for the request. + * + * @param uri the target URI + * @return prepared invocation builder + */ + protected Invocation.Builder prepareClient(URI uri) { + Client client; + if (this.configuredClient == null) { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (null != this.clientConfig) { + clientBuilder.withConfig(this.clientConfig); + } + if (null != this.sslContext) { + clientBuilder.sslContext(this.sslContext); + } + client = clientBuilder.build(); + } else { + client = this.configuredClient; + } + + return client + .register(new LoggingFilter()) + .target(uri) + .request() + .accept(APPLICATION_JSON_TYPE) + .header("User-Agent", buildUserAgentString()); + } + + /** + * Build user-agent string. + * + * @return user-agent string in the format: smart-id-java-client/[client-version] (Java/[java-version]) + */ + protected String buildUserAgentString() { + return "smart-id-java-client/" + getClientVersion() + " (Java/" + getJdkMajorVersion() + ")"; + } + + /** + * Get client version from package implementation version. + * + * @return client version or "-" + */ + protected String getClientVersion() { + String clientVersion = getClass().getPackage().getImplementationVersion(); + return clientVersion == null ? "-" : clientVersion; + } + + /** + * Get JDK major version. + * + * @return JDK major version or "-" + */ + protected String getJdkMajorVersion() { + try { + return System.getProperty("java.version").split("_")[0]; + } catch (Exception e) { + return "-"; + } + } + + private T postRequest(URI uri, V request, Class responseType) { + try { + Entity requestEntity = Entity.entity(request, MediaType.APPLICATION_JSON); + return prepareClient(uri).post(requestEntity, responseType); + } catch (NotAuthorizedException ex) { + logger.warn("Request is unauthorized for URI {}", uri, ex); + throw new RelyingPartyAccountConfigurationException("Request is unauthorized for URI " + uri, ex); + } catch (BadRequestException ex) { + logger.warn("Request is invalid for URI {}", uri, ex); + throw new SmartIdClientException("Server refused the request", ex); + } catch (NotFoundException e) { + logger.warn("User account not found for URI " + uri, e); + throw new UserAccountNotFoundException(); + } catch (ForbiddenException ex) { + logger.warn("No permission to issue the request", ex); + throw new RelyingPartyAccountConfigurationException("No permission to issue the request", ex); + } catch (ClientErrorException ex) { + if (ex.getResponse().getStatus() == 471) { + logger.warn("No suitable account of requested type found, but user has some other accounts.", ex); + throw new NoSuitableAccountOfRequestedTypeFoundException(); + } + if (ex.getResponse().getStatus() == 472) { + logger.warn("Person should view Smart-ID app or Smart-ID self-service portal now.", ex); + throw new PersonShouldViewSmartIdPortalException(); + } + if (ex.getResponse().getStatus() == 480) { + logger.warn("Client-side API is too old and not supported anymore"); + throw new SmartIdClientException("Client-side API is too old and not supported anymore"); + } + throw ex; + } catch (ServerErrorException ex) { + if (ex.getResponse().getStatus() == 580) { + logger.warn("Server is under maintenance, retry later", ex); + throw new ServerMaintenanceException(); + } + throw ex; + } + } + + private SessionStatusRequest createSessionStatusRequest(String sessionId) { + var request = new SessionStatusRequest(sessionId); + if (sessionStatusResponseSocketOpenTimeUnit != null && sessionStatusResponseSocketOpenTimeValue > 0) { + request.setResponseSocketOpenTime(sessionStatusResponseSocketOpenTimeUnit, sessionStatusResponseSocketOpenTimeValue); + } + return request; + } + + private void addResponseSocketOpenTimeUrlParameter(SessionStatusRequest request, UriBuilder uriBuilder) { + if (request.isResponseSocketOpenTimeSet()) { + TimeUnit timeUnit = request.getResponseSocketOpenTimeUnit(); + long timeValue = request.getResponseSocketOpenTimeValue(); + long queryTimeoutInMilliseconds = timeUnit.toMillis(timeValue); + uriBuilder.queryParam("timeoutMs", queryTimeoutInMilliseconds); + } + } } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java index 0332f4b7..b2bf44c4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/AcspV2SignatureProtocolParameters.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * ACSP_V2 signature protocol parameters - * - * @param rpChallenge Required. The RP challenge in Base64 encoding - * @param signatureAlgorithm Required. The signature algorithm. Only supported value is rsassa-pss - * @param signatureAlgorithmParameters Required. The signature algorithm parameters - */ -public record AcspV2SignatureProtocolParameters(String rpChallenge, - String signatureAlgorithm, - SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * ACSP_V2 signature protocol parameters + * + * @param rpChallenge Required. The RP challenge in Base64 encoding + * @param signatureAlgorithm Required. The signature algorithm. Only supported value is rsassa-pss + * @param signatureAlgorithmParameters Required. The signature algorithm parameters + */ +public record AcspV2SignatureProtocolParameters(String rpChallenge, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java index bb94c73a..443d2177 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateByDocumentNumberRequest.java @@ -1,43 +1,43 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request for querying certificate by document number. - * - * @param relyingPartyUUID Required. The relying party UUID - * @param relyingPartyName Required. The relying party name - * @param certificateLevel The certificate level. Possible values are "QSCD", "QUALIFIED" and "ADVANCED". If not specified, defaults to "QUALIFIED" - */ -public record CertificateByDocumentNumberRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request for querying certificate by document number. + * + * @param relyingPartyUUID Required. The relying party UUID + * @param relyingPartyName Required. The relying party name + * @param certificateLevel The certificate level. Possible values are "QSCD", "QUALIFIED" and "ADVANCED". If not specified, defaults to "QUALIFIED" + */ +public record CertificateByDocumentNumberRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java index 369a8c9f..55b68605 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateInfo.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Certificate info - * - * @param value Required. The certificate data in Base64-encoded format. - * @param certificateLevel Required. The certificate level, e.g. "QUALIFIED" or "ADVANCED". - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record CertificateInfo(String value, String certificateLevel) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Certificate info + * + * @param value Required. The certificate data in Base64-encoded format. + * @param certificateLevel Required. The certificate level, e.g. "QUALIFIED" or "ADVANCED". + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record CertificateInfo(String value, String certificateLevel) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java index fdce7dc2..9bc32de6 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/CertificateResponse.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Response of certificate queried by document number - * - * @param state Required. State of the certificate - * @param cert Required if state is OK. Certificate information {@link CertificateInfo} - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Response of certificate queried by document number + * + * @param state Required. State of the certificate + * @param cert Required if state is OK. Certificate information {@link CertificateInfo} + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record CertificateResponse(String state, CertificateInfo cert) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java index 0b61c316..abc502a4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkAuthenticationSessionRequest.java @@ -1,57 +1,57 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; -import ee.sk.smartid.SignatureProtocol; - -/** - * Device link authentication session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for authentication. - * @param signatureProtocol Required. Authentication signature protocol to be used - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param interactions Required. Interaction to be used in the authentication session - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that the client could use - * @param initialCallbackUrl URL to which the user will be redirected. - */ -public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - SignatureProtocol signatureProtocol, - AcspV2SignatureProtocolParameters signatureProtocolParameters, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ee.sk.smartid.SignatureProtocol; + +/** + * Device link authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Authentication signature protocol to be used + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + SignatureProtocol signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java index 01092a68..0bfd03e5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkCertificateChoiceSessionRequest.java @@ -1,54 +1,54 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request to create a Device Link session for choosing a certificate. - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param requestProperties Additional request properties - * @param initialCallbackUrl Initial callback URL to be used instead of the default one configured for the RP. - */ -public record DeviceLinkCertificateChoiceSessionRequest( - String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { - +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request to create a Device Link session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + * @param initialCallbackUrl Initial callback URL to be used instead of the default one configured for the RP. + */ +public record DeviceLinkCertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { + } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java index ef012808..55b0f039 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSessionResponse.java @@ -1,71 +1,71 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.net.URI; -import java.time.Instant; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Response of session creation for device link flows - * - * @param sessionID Required. The unique identifier of the session. - * @param sessionToken Required. The token of the session. - * @param sessionSecret Required. The secret for the session. - * @param deviceLinkBase Required. Base URI for generating device link. - * @param receivedAt Timestamp when the response was received. - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record DeviceLinkSessionResponse(String sessionID, - String sessionToken, - String sessionSecret, - URI deviceLinkBase, - Instant receivedAt) implements Serializable { - - /** - * Initializes a new instance of the {@link DeviceLinkSessionResponse} class. - *

- * The receivedAt value is set to the current time. - * - * @param sessionID Required. The unique identifier of the session. - * @param sessionToken Required. The token of the session. - * @param sessionSecret Required. The secret for the session. - * @param deviceLinkBase Required. Base URI for generating device link - */ - @JsonCreator - public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, - @JsonProperty("sessionToken") String sessionToken, - @JsonProperty("sessionSecret") String sessionSecret, - @JsonProperty("deviceLinkBase") URI deviceLinkBase) { - this(sessionID, sessionToken, sessionSecret, deviceLinkBase, Instant.now()); - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.net.URI; +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response of session creation for device link flows + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link. + * @param receivedAt Timestamp when the response was received. + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record DeviceLinkSessionResponse(String sessionID, + String sessionToken, + String sessionSecret, + URI deviceLinkBase, + Instant receivedAt) implements Serializable { + + /** + * Initializes a new instance of the {@link DeviceLinkSessionResponse} class. + *

+ * The receivedAt value is set to the current time. + * + * @param sessionID Required. The unique identifier of the session. + * @param sessionToken Required. The token of the session. + * @param sessionSecret Required. The secret for the session. + * @param deviceLinkBase Required. Base URI for generating device link + */ + @JsonCreator + public DeviceLinkSessionResponse(@JsonProperty("sessionID") String sessionID, + @JsonProperty("sessionToken") String sessionToken, + @JsonProperty("sessionSecret") String sessionSecret, + @JsonProperty("deviceLinkBase") URI deviceLinkBase) { + this(sessionID, sessionToken, sessionSecret, deviceLinkBase, Instant.now()); + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java index 7602b237..e1f16fa2 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/DeviceLinkSignatureSessionRequest.java @@ -1,58 +1,58 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Device link-based signature session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for signing. - * @param signatureProtocol Required. Signature protocol to be used for signing. - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param interactions Required. Interaction to be used in the signature session - * @param requestProperties Additional properties for the request - * @param initialCallbackUrl URL to which the user will be redirected. - */ -public record DeviceLinkSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Device link-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + * @param initialCallbackUrl URL to which the user will be redirected. + */ +public record DeviceLinkSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) String initialCallbackUrl) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java index 5d71e43d..2e84645d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/Interaction.java +++ b/src/main/java/ee/sk/smartid/rest/dao/Interaction.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Interaction to be used in authentication and signing requests - * - * @param type Required. The interaction type - * @param displayText60 Requirement depends on the type. The text to be displayed on the device screen (maximum length 60 characters). - * @param displayText200 Requirement depends on the type. the text to be displayed on the device screen (maximum length 200 characters). - */ -public record Interaction(String type, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText60, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText200) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Interaction to be used in authentication and signing requests + * + * @param type Required. The interaction type + * @param displayText60 Requirement depends on the type. The text to be displayed on the device screen (maximum length 60 characters). + * @param displayText200 Requirement depends on the type. the text to be displayed on the device screen (maximum length 200 characters). + */ +public record Interaction(String type, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText60, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String displayText200) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java index ce9131bd..ebad4ab8 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionRequest.java @@ -1,57 +1,57 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Linked signature session request - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Certificate level. Possible values: QSCD, QUALIFIED, ADVANCED, - * @param signatureProtocol Required. Signature protocol. Only RAW_DIGEST_SIGNATURE is supported for signing. - * @param signatureProtocolParameters Required. RAW_DIGEST_SIGNATURE signature protocol parameters - * @param linkedSessionID Required. ID of the anonymous certificate choice session to be linked with this signature session. - * @param nonce Random value to cancel out idempotence of the request. - * @param interactions Required. Device link interactions should be used. - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - */ -public record LinkedSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - String linkedSessionID, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Linked signature session request + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Certificate level. Possible values: QSCD, QUALIFIED, ADVANCED, + * @param signatureProtocol Required. Signature protocol. Only RAW_DIGEST_SIGNATURE is supported for signing. + * @param signatureProtocolParameters Required. RAW_DIGEST_SIGNATURE signature protocol parameters + * @param linkedSessionID Required. ID of the anonymous certificate choice session to be linked with this signature session. + * @param nonce Random value to cancel out idempotence of the request. + * @param interactions Required. Device link interactions should be used. + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + */ +public record LinkedSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + String linkedSessionID, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java index 3c3cecaa..eede94a4 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/LinkedSignatureSessionResponse.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Response for linked notification based signature session initiation. - * - * @param sessionID The session ID - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record LinkedSignatureSessionResponse(String sessionID) { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Response for linked notification based signature session initiation. + * + * @param sessionID The session ID + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record LinkedSignatureSessionResponse(String sessionID) { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java index 95678a50..316bf206 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionRequest.java @@ -1,56 +1,56 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Notification-based authentication session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for authentication. - * @param signatureProtocol Required. Signature protocol to be used for authentication - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param interactions Required. Interaction to be used in the authentication session - * @param requestProperties Additional properties for the request - * @param capabilities Capabilities that the client could use - * @param vcType Required. Verification code type to be used in the authentication session - */ -public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - AcspV2SignatureProtocolParameters signatureProtocolParameters, - String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - String vcType) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based authentication session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for authentication. + * @param signatureProtocol Required. Signature protocol to be used for authentication + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param interactions Required. Interaction to be used in the authentication session + * @param requestProperties Additional properties for the request + * @param capabilities Capabilities that the client could use + * @param vcType Required. Verification code type to be used in the authentication session + */ +public record NotificationAuthenticationSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + AcspV2SignatureProtocolParameters signatureProtocolParameters, + String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + String vcType) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java index c28b1e6b..1cb13f47 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationAuthenticationSessionResponse.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based authentication session response - * - * @param sessionID the ID of the created authentication session - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationAuthenticationSessionResponse(String sessionID) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based authentication session response + * + * @param sessionID the ID of the created authentication session + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationAuthenticationSessionResponse(String sessionID) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java index 10946bd2..a665abde 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionRequest.java @@ -1,52 +1,52 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Request to create a notification-based session for choosing a certificate. - * - * @param relyingPartyUUID Required. Relying party UUID - * @param relyingPartyName Required. Relying party name - * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param requestProperties Additional request properties - */ -public record NotificationCertificateChoiceSessionRequest( - String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Request to create a notification-based session for choosing a certificate. + * + * @param relyingPartyUUID Required. Relying party UUID + * @param relyingPartyName Required. Relying party name + * @param certificateLevel Requested certificate level. If not specified, will default to QUALIFIED. + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param requestProperties Additional request properties + */ +public record NotificationCertificateChoiceSessionRequest( + String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java index ecbec0da..77e943ea 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationCertificateChoiceSessionResponse.java @@ -1,40 +1,40 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based certificate choice response - * - * @param sessionID Required. The ID of the created certificate choice session. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationCertificateChoiceSessionResponse(String sessionID) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based certificate choice response + * + * @param sessionID Required. The ID of the created certificate choice session. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationCertificateChoiceSessionResponse(String sessionID) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java index c99ef0a5..7abdd7e9 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionRequest.java @@ -1,56 +1,56 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Notification-based signature session request - * - * @param relyingPartyUUID Required. The unique identifier of the relying party. - * @param relyingPartyName Required. The name of the relying party - * @param certificateLevel Certificate level to be requested for signing. - * @param signatureProtocol Required. Signature protocol to be used for signing. - * @param signatureProtocolParameters Required. Parameters for the selected signature protocol - * @param nonce Random value that can be used to override idempotent behaviour - * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. - * @param interactions Required. Interaction to be used in the signature session - * @param requestProperties Additional properties for the request - */ -public record NotificationSignatureSessionRequest(String relyingPartyUUID, - String relyingPartyName, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, - String signatureProtocol, - RawDigestSignatureProtocolParameters signatureProtocolParameters, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, - @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, - @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, - @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Notification-based signature session request + * + * @param relyingPartyUUID Required. The unique identifier of the relying party. + * @param relyingPartyName Required. The name of the relying party + * @param certificateLevel Certificate level to be requested for signing. + * @param signatureProtocol Required. Signature protocol to be used for signing. + * @param signatureProtocolParameters Required. Parameters for the selected signature protocol + * @param nonce Random value that can be used to override idempotent behaviour + * @param capabilities Capabilities that should be used only when agreed with the Smart-ID provider. + * @param interactions Required. Interaction to be used in the signature session + * @param requestProperties Additional properties for the request + */ +public record NotificationSignatureSessionRequest(String relyingPartyUUID, + String relyingPartyName, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String certificateLevel, + String signatureProtocol, + RawDigestSignatureProtocolParameters signatureProtocolParameters, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String nonce, + @JsonInclude(JsonInclude.Include.NON_NULL) Set capabilities, + @JsonInclude(JsonInclude.Include.NON_EMPTY) String interactions, + @JsonInclude(JsonInclude.Include.NON_NULL) RequestProperties requestProperties) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java index edd7e335..9085d2d1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java +++ b/src/main/java/ee/sk/smartid/rest/dao/NotificationSignatureSessionResponse.java @@ -1,42 +1,42 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Notification-based signature session request - * - * @param sessionID Required. The ID of the created signature session. - * @param vc Required. Verification code details - */ - -@JsonIgnoreProperties(ignoreUnknown = true) -public record NotificationSignatureSessionResponse(String sessionID, VerificationCode vc) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Notification-based signature session request + * + * @param sessionID Required. The ID of the created signature session. + * @param vc Required. Verification code details + */ + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NotificationSignatureSessionResponse(String sessionID, VerificationCode vc) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java index 616a3e99..16e9fe9b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RawDigestSignatureProtocolParameters.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Parameters for protocol RAW_DIGEST_SIGNATURE - * - * @param digest Required. The digest to be signed, Base64 encoded. - * @param signatureAlgorithm Required. The signature algorithm. Supported value is RSASSA-PSS. - * @param signatureAlgorithmParameters Required. The parameters for signature algorithm. - */ -public record RawDigestSignatureProtocolParameters(String digest, - String signatureAlgorithm, - SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Parameters for protocol RAW_DIGEST_SIGNATURE + * + * @param digest Required. The digest to be signed, Base64 encoded. + * @param signatureAlgorithm Required. The signature algorithm. Supported value is RSASSA-PSS. + * @param signatureAlgorithmParameters Required. The parameters for signature algorithm. + */ +public record RawDigestSignatureProtocolParameters(String digest, + String signatureAlgorithm, + SignatureAlgorithmParameters signatureAlgorithmParameters) implements Serializable { } \ No newline at end of file diff --git a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java index d46700f2..c4832677 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java +++ b/src/main/java/ee/sk/smartid/rest/dao/RequestProperties.java @@ -1,39 +1,39 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -/** - * Additional request properties - * - * @param shareMdClientIpAddress Set if the client's device IP address should be provided in sessions status response - */ -public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Additional request properties + * + * @param shareMdClientIpAddress Set if the client's device IP address should be provided in sessions status response + */ +public record RequestProperties(@JsonInclude(JsonInclude.Include.NON_NULL) Boolean shareMdClientIpAddress) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java index c8f3b9cc..ec8b1886 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SemanticsIdentifier.java @@ -1,138 +1,138 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Representation of Semantic Identifier. - */ -public class SemanticsIdentifier implements Serializable { - - private final String identifier; - - /** - * Constructs a new SemanticsIdentifier with the specified identity type, country code and identity number. - * - * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} - * @param countryCode the country code (e.g., EE, LT, LV). See {@link CountryCode} - * @param identityNumber the identity number - */ - public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { - this.identifier = "" + identityType + countryCode + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identity type, country code string and identity number. - * - * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} - * @param countryCodeString country code as string (e.g., EE, LT, LV) - * @param identityNumber the identity number - */ - public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { - this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identity type string, country code string and identity number. - * - * @param identityTypeString the identity type as string (e.g., PAS, IDC, PNO) - * @param countryCodeString country code as string (e.g., EE, LT, LV) - * @param identityNumber the identity number - */ - public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { - this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; - } - - /** - * Constructs a new SemanticsIdentifier with the specified identifier string. - * - * @param identifier the full semantics identifier string (e.g., "PAS EE-1234567890") - */ - public SemanticsIdentifier(String identifier) { - this.identifier = identifier; - } - - /** - * Gets the full semantics identifier string. - * - * @return the full semantics identifier string - */ - public String getIdentifier() { - return identifier; - } - - /** - * 3-character identity type codes for SemanticsIdentifier - */ - public enum IdentityType { - - /** - * PAS - Passport - */ - PAS, - - /** - * IDC - Identity Card - */ - IDC, - - /** - * PNO - Personal Number - */ - PNO - } - - /** - * 2-character country codes for SemanticsIdentifier - */ - public enum CountryCode { - - /** - * Estonia - */ - EE, - - /** - * Lithuania - */ - LT, - - /** - * Latvia - */ - LV - } - - @Override - public String toString() { - return "SemanticsIdentifier{" + - "identifier='" + identifier + '\'' + - '}'; - } - -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Representation of Semantic Identifier. + */ +public class SemanticsIdentifier implements Serializable { + + private final String identifier; + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCode the country code (e.g., EE, LT, LV). See {@link CountryCode} + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, CountryCode countryCode, String identityNumber) { + this.identifier = "" + identityType + countryCode + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type, country code string and identity number. + * + * @param identityType the identity type (e.g., PAS, IDC, PNO). See {@link IdentityType} + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(IdentityType identityType, String countryCodeString, String identityNumber) { + this.identifier = "" + identityType + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identity type string, country code string and identity number. + * + * @param identityTypeString the identity type as string (e.g., PAS, IDC, PNO) + * @param countryCodeString country code as string (e.g., EE, LT, LV) + * @param identityNumber the identity number + */ + public SemanticsIdentifier(String identityTypeString, String countryCodeString, String identityNumber) { + this.identifier = "" + identityTypeString + countryCodeString + "-" + identityNumber; + } + + /** + * Constructs a new SemanticsIdentifier with the specified identifier string. + * + * @param identifier the full semantics identifier string (e.g., "PAS EE-1234567890") + */ + public SemanticsIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Gets the full semantics identifier string. + * + * @return the full semantics identifier string + */ + public String getIdentifier() { + return identifier; + } + + /** + * 3-character identity type codes for SemanticsIdentifier + */ + public enum IdentityType { + + /** + * PAS - Passport + */ + PAS, + + /** + * IDC - Identity Card + */ + IDC, + + /** + * PNO - Personal Number + */ + PNO + } + + /** + * 2-character country codes for SemanticsIdentifier + */ + public enum CountryCode { + + /** + * Estonia + */ + EE, + + /** + * Lithuania + */ + LT, + + /** + * Latvia + */ + LV + } + + @Override + public String toString() { + return "SemanticsIdentifier{" + + "identifier='" + identifier + '\'' + + '}'; + } + +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java index ec4434e1..1a3accf1 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionCertificate.java @@ -1,80 +1,80 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Certificate data in session status response. - *

- * value - the certificate data in Base64-encoded format - * certificateLevel - the certificate level. Possible values: QUALIFIED or ADVANCED - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionCertificate implements Serializable { - - private String value; - private String certificateLevel; - - /** - * Get the certificate value. - * - * @return the certificate data in Base64-encoded format - */ - public String getValue() { - return value; - } - - /** - * Set the certificate value. - * - * @param value the certificate data in Base64-encoded format - */ - public void setValue(String value) { - this.value = value; - } - - /** - * Gets the certificate level. - * - * @return the certificate level - */ - public String getCertificateLevel() { - return certificateLevel; - } - - /** - * Sets the certificate level. - * - * @param certificateLevel the certificate level - */ - public void setCertificateLevel(String certificateLevel) { - this.certificateLevel = certificateLevel; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Certificate data in session status response. + *

+ * value - the certificate data in Base64-encoded format + * certificateLevel - the certificate level. Possible values: QUALIFIED or ADVANCED + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionCertificate implements Serializable { + + private String value; + private String certificateLevel; + + /** + * Get the certificate value. + * + * @return the certificate data in Base64-encoded format + */ + public String getValue() { + return value; + } + + /** + * Set the certificate value. + * + * @param value the certificate data in Base64-encoded format + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the certificate level. + * + * @return the certificate level + */ + public String getCertificateLevel() { + return certificateLevel; + } + + /** + * Sets the certificate level. + * + * @param certificateLevel the certificate level + */ + public void setCertificateLevel(String certificateLevel) { + this.certificateLevel = certificateLevel; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java index e1844752..241378f5 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithm.java @@ -1,80 +1,80 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Mask generation algorithm data in session status response. - *

- * algorithm - Required. The algorithm name, e.g. "id-mgf1" - * parameters - Required. The mask generation algorithm parameters - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionMaskGenAlgorithm implements Serializable { - - private String algorithm; - private SessionMaskGenAlgorithmParameters parameters; - - /** - * Gets the algorithm. - * - * @return the algorithm - */ - public String getAlgorithm() { - return algorithm; - } - - /** - * Sets the algorithm. - * - * @param algorithm the algorithm - */ - public void setAlgorithm(String algorithm) { - this.algorithm = algorithm; - } - - /** - * Gets the parameters. - * - * @return the parameters - */ - public SessionMaskGenAlgorithmParameters getParameters() { - return parameters; - } - - /** - * Sets the parameters. - * - * @param parameters the parameters - */ - public void setParameters(SessionMaskGenAlgorithmParameters parameters) { - this.parameters = parameters; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Mask generation algorithm data in session status response. + *

+ * algorithm - Required. The algorithm name, e.g. "id-mgf1" + * parameters - Required. The mask generation algorithm parameters + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithm implements Serializable { + + private String algorithm; + private SessionMaskGenAlgorithmParameters parameters; + + /** + * Gets the algorithm. + * + * @return the algorithm + */ + public String getAlgorithm() { + return algorithm; + } + + /** + * Sets the algorithm. + * + * @param algorithm the algorithm + */ + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + /** + * Gets the parameters. + * + * @return the parameters + */ + public SessionMaskGenAlgorithmParameters getParameters() { + return parameters; + } + + /** + * Sets the parameters. + * + * @param parameters the parameters + */ + public void setParameters(SessionMaskGenAlgorithmParameters parameters) { + this.parameters = parameters; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java index 1096caaf..029782ba 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionMaskGenAlgorithmParameters.java @@ -1,60 +1,60 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Mask generation algorithm parameters. - *

- * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionMaskGenAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - - /** - * Gets hash algorithm. - * - * @return hash algorithm - */ - public String getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Sets hash algorithm. - * - * @param hashAlgorithm hash algorithm - */ - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Mask generation algorithm parameters. + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionMaskGenAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ + public String getHashAlgorithm() { + return hashAlgorithm; + } + + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java index 37e23c37..f7c2ca6b 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResult.java @@ -1,101 +1,101 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents how session ended - successfully, cancelled by user, timed out, etc. - * Available when session state is COMPLETE. - *

- * endResult - Required. Reason for the session state being COMPLETED. - * documentNumber - Required. User's document number - * details - Additional details if user refused interaction. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionResult implements Serializable { - - private String endResult; - private String documentNumber; - private SessionResultDetails details; - - /** - * Get exact end result of the session. - * - * @return end result of the session - */ - public String getEndResult() { - return endResult; - } - - /** - * Set end result of the session - * - * @param endResult end result of the session - */ - public void setEndResult(String endResult) { - this.endResult = endResult; - } - - /** - * Get document number of the user used in the session. - * - * @return document number of the user - */ - public String getDocumentNumber() { - return documentNumber; - } - - /** - * Set document number of the user - * - * @param documentNumber document number of the user - */ - public void setDocumentNumber(String documentNumber) { - this.documentNumber = documentNumber; - } - - /** - * Get additional details - * - * @return details of the session result - */ - public SessionResultDetails getDetails() { - return details; - } - - /** - * Set details of the session result - * - * @param details details of the session result - */ - public void setDetails(SessionResultDetails details) { - this.details = details; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents how session ended - successfully, cancelled by user, timed out, etc. + * Available when session state is COMPLETE. + *

+ * endResult - Required. Reason for the session state being COMPLETED. + * documentNumber - Required. User's document number + * details - Additional details if user refused interaction. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResult implements Serializable { + + private String endResult; + private String documentNumber; + private SessionResultDetails details; + + /** + * Get exact end result of the session. + * + * @return end result of the session + */ + public String getEndResult() { + return endResult; + } + + /** + * Set end result of the session + * + * @param endResult end result of the session + */ + public void setEndResult(String endResult) { + this.endResult = endResult; + } + + /** + * Get document number of the user used in the session. + * + * @return document number of the user + */ + public String getDocumentNumber() { + return documentNumber; + } + + /** + * Set document number of the user + * + * @param documentNumber document number of the user + */ + public void setDocumentNumber(String documentNumber) { + this.documentNumber = documentNumber; + } + + /** + * Get additional details + * + * @return details of the session result + */ + public SessionResultDetails getDetails() { + return details; + } + + /** + * Set details of the session result + * + * @param details details of the session result + */ + public void setDetails(SessionResultDetails details) { + this.details = details; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java index 56a0f08c..26e82578 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionResultDetails.java @@ -1,62 +1,62 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents additional info when end result if user refused interactions. - *

- * Required when end result is USER_REFUSED_INTERACTION. - *

- * interaction - Type of the interaction that was cancelled by the user, e.g. "displayTextAndPIN" - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionResultDetails implements Serializable { - - private String interaction; - - /** - * Gets type of the interaction that was cancelled by the user. - * - * @return type of the interaction that was cancelled by the user. - */ - public String getInteraction() { - return interaction; - } - - /** - * Sets type of the interaction type - * - * @param interaction type of the interaction - */ - public void setInteraction(String interaction) { - this.interaction = interaction; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents additional info when end result if user refused interactions. + *

+ * Required when end result is USER_REFUSED_INTERACTION. + *

+ * interaction - Type of the interaction that was cancelled by the user, e.g. "displayTextAndPIN" + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionResultDetails implements Serializable { + + private String interaction; + + /** + * Gets type of the interaction that was cancelled by the user. + * + * @return type of the interaction that was cancelled by the user. + */ + public String getInteraction() { + return interaction; + } + + /** + * Sets type of the interaction type + * + * @param interaction type of the interaction + */ + public void setInteraction(String interaction) { + this.interaction = interaction; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java index a2c634cb..8bd48aad 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignature.java @@ -1,160 +1,160 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Signature data. - *

- * value - Required. Signature value in Base64-encoded format. - * serverRandom - Required. Server random value in Base64-encoded format. - * userChallenge - User challenge value in URL-safe Base64-encoded format. - * flowType - Required. The flow type, e.g. "QR", "Web2App". - * signatureAlgorithm - Required. The signature algorithm, e.g. "rsassa-pss". - * signatureAlgorithmParameters - Required. The signature algorithm parameters. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionSignature implements Serializable { - - private String value; - private String serverRandom; - private String userChallenge; - private String flowType; - private String signatureAlgorithm; - private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; - - /** - * Get the signature value. - * - * @return the signature value - */ - public String getValue() { - return value; - } - - /** - * Set the signature value. - * - * @param value the signature value - */ - public void setValue(String value) { - this.value = value; - } - - /** - * Get the server random value. - * - * @return the server random value - */ - public String getServerRandom() { - return serverRandom; - } - - /** - * Set the server random value. - * - * @param serverRandom the server random value - */ - public void setServerRandom(String serverRandom) { - this.serverRandom = serverRandom; - } - - /** - * Get the user challenge value. - * - * @return the user challenge value - */ - public String getUserChallenge() { - return userChallenge; - } - - /** - * Set the user challenge value. - * - * @param userChallenge the user challenge value - */ - public void setUserChallenge(String userChallenge) { - this.userChallenge = userChallenge; - } - - /** - * Get the flow type. - * - * @return the flow type - */ - public String getFlowType() { - return flowType; - } - - /** - * Set the flow type. - * - * @param flowType the flow type - */ - public void setFlowType(String flowType) { - this.flowType = flowType; - } - - /** - * Get the signature algorithm. - * - * @return the signature algorithm - */ - public String getSignatureAlgorithm() { - return signatureAlgorithm; - } - - /** - * Set the signature algorithm. - * - * @param signatureAlgorithm the signature algorithm - */ - public void setSignatureAlgorithm(String signatureAlgorithm) { - this.signatureAlgorithm = signatureAlgorithm; - } - - /** - * Get the signature algorithm parameters. - * - * @return the signature algorithm parameters - */ - public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { - return signatureAlgorithmParameters; - } - - /** - * Set the signature algorithm parameters. - * - * @param signatureAlgorithmParameters the signature algorithm parameters - */ - public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { - this.signatureAlgorithmParameters = signatureAlgorithmParameters; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Signature data. + *

+ * value - Required. Signature value in Base64-encoded format. + * serverRandom - Required. Server random value in Base64-encoded format. + * userChallenge - User challenge value in URL-safe Base64-encoded format. + * flowType - Required. The flow type, e.g. "QR", "Web2App". + * signatureAlgorithm - Required. The signature algorithm, e.g. "rsassa-pss". + * signatureAlgorithmParameters - Required. The signature algorithm parameters. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignature implements Serializable { + + private String value; + private String serverRandom; + private String userChallenge; + private String flowType; + private String signatureAlgorithm; + private SessionSignatureAlgorithmParameters signatureAlgorithmParameters; + + /** + * Get the signature value. + * + * @return the signature value + */ + public String getValue() { + return value; + } + + /** + * Set the signature value. + * + * @param value the signature value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Get the server random value. + * + * @return the server random value + */ + public String getServerRandom() { + return serverRandom; + } + + /** + * Set the server random value. + * + * @param serverRandom the server random value + */ + public void setServerRandom(String serverRandom) { + this.serverRandom = serverRandom; + } + + /** + * Get the user challenge value. + * + * @return the user challenge value + */ + public String getUserChallenge() { + return userChallenge; + } + + /** + * Set the user challenge value. + * + * @param userChallenge the user challenge value + */ + public void setUserChallenge(String userChallenge) { + this.userChallenge = userChallenge; + } + + /** + * Get the flow type. + * + * @return the flow type + */ + public String getFlowType() { + return flowType; + } + + /** + * Set the flow type. + * + * @param flowType the flow type + */ + public void setFlowType(String flowType) { + this.flowType = flowType; + } + + /** + * Get the signature algorithm. + * + * @return the signature algorithm + */ + public String getSignatureAlgorithm() { + return signatureAlgorithm; + } + + /** + * Set the signature algorithm. + * + * @param signatureAlgorithm the signature algorithm + */ + public void setSignatureAlgorithm(String signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Get the signature algorithm parameters. + * + * @return the signature algorithm parameters + */ + public SessionSignatureAlgorithmParameters getSignatureAlgorithmParameters() { + return signatureAlgorithmParameters; + } + + /** + * Set the signature algorithm parameters. + * + * @param signatureAlgorithmParameters the signature algorithm parameters + */ + public void setSignatureAlgorithmParameters(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { + this.signatureAlgorithmParameters = signatureAlgorithmParameters; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java index 2f008632..5fbcee83 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionSignatureAlgorithmParameters.java @@ -1,120 +1,120 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Signature algorithm parameters - *

- * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" - * maskGenAlgorithm - Required. The mask generation algorithm - * saltLength - Required. The salt length, e.g. 32 for SHA-256 - * trailerField - Required. The trailer field, e.g. "0xbc"> - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionSignatureAlgorithmParameters implements Serializable { - - private String hashAlgorithm; - private SessionMaskGenAlgorithm maskGenAlgorithm; - private Integer saltLength; - private String trailerField; - - /** - * Gets hash algorithm. - * - * @return hash algorithm - */ - public String getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Sets hash algorithm. - * - * @param hashAlgorithm hash algorithm - */ - public void setHashAlgorithm(String hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Gets mask generation algorithm. - * - * @return mask generation algorithm - */ - public SessionMaskGenAlgorithm getMaskGenAlgorithm() { - return maskGenAlgorithm; - } - - /** - * Sets mask generation algorithm. - * - * @param maskGenAlgorithm mask generation algorithm - */ - public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { - this.maskGenAlgorithm = maskGenAlgorithm; - } - - /** - * Gets salt length. - * - * @return salt length - */ - public Integer getSaltLength() { - return saltLength; - } - - /** - * Sets salt length. - * - * @param saltLength salt length - */ - public void setSaltLength(Integer saltLength) { - this.saltLength = saltLength; - } - - /** - * Gets trailer field. - * - * @return trailer field - */ - public String getTrailerField() { - return trailerField; - } - - /** - * Sets trailer field. - * - * @param trailerField trailer field - */ - public void setTrailerField(String trailerField) { - this.trailerField = trailerField; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Signature algorithm parameters + *

+ * hashAlgorithm - Required. The hash algorithm, e.g. "SHA-256" + * maskGenAlgorithm - Required. The mask generation algorithm + * saltLength - Required. The salt length, e.g. 32 for SHA-256 + * trailerField - Required. The trailer field, e.g. "0xbc"> + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionSignatureAlgorithmParameters implements Serializable { + + private String hashAlgorithm; + private SessionMaskGenAlgorithm maskGenAlgorithm; + private Integer saltLength; + private String trailerField; + + /** + * Gets hash algorithm. + * + * @return hash algorithm + */ + public String getHashAlgorithm() { + return hashAlgorithm; + } + + /** + * Sets hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ + public void setHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + /** + * Gets mask generation algorithm. + * + * @return mask generation algorithm + */ + public SessionMaskGenAlgorithm getMaskGenAlgorithm() { + return maskGenAlgorithm; + } + + /** + * Sets mask generation algorithm. + * + * @param maskGenAlgorithm mask generation algorithm + */ + public void setMaskGenAlgorithm(SessionMaskGenAlgorithm maskGenAlgorithm) { + this.maskGenAlgorithm = maskGenAlgorithm; + } + + /** + * Gets salt length. + * + * @return salt length + */ + public Integer getSaltLength() { + return saltLength; + } + + /** + * Sets salt length. + * + * @param saltLength salt length + */ + public void setSaltLength(Integer saltLength) { + this.saltLength = saltLength; + } + + /** + * Gets trailer field. + * + * @return trailer field + */ + public String getTrailerField() { + return trailerField; + } + + /** + * Sets trailer field. + * + * @param trailerField trailer field + */ + public void setTrailerField(String trailerField) { + this.trailerField = trailerField; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java index 88738e06..59641acc 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatus.java @@ -1,200 +1,200 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Represents response for active session query. - *

- * state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE"> - * result - Required if state is "COMPLETE". Details about how session ended. - * signatureProtocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE". - * signature - Required if end result is OK. Signature data containing the actual signature and related information. - * cert - Required if end result is OK. Signer's certificate data. - * ignoredProperties - properties that were ignored from the session request. - * interactionTypeUsed - Required if end result is OK. Interaction type that was used in the session. - * deviceIpAddress - IP address of the device used in the session. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SessionStatus implements Serializable { - - private String state; - private SessionResult result; - private String signatureProtocol; - private SessionSignature signature; - private SessionCertificate cert; - private String[] ignoredProperties; - private String interactionTypeUsed; - private String deviceIpAddress; - - /** - * Get state of the session - * - * @return state of the session - */ - public String getState() { - return state; - } - - /** - * Set state of the session - * - * @param state state of the session - */ - public void setState(String state) { - this.state = state; - } - - /** - * Get result of the session - * - * @return result of the session - */ - public SessionResult getResult() { - return result; - } - - /** - * Set result of the session - * - * @param result result of the session - */ - public void setResult(SessionResult result) { - this.result = result; - } - - /** - * Get signature protocol used - * - * @return signature protocol used - */ - public String getSignatureProtocol() { - return signatureProtocol; - } - - /** - * Sets the signature protocol used - * - * @param signatureProtocol signature protocol used - */ - public void setSignatureProtocol(String signatureProtocol) { - this.signatureProtocol = signatureProtocol; - } - - /** - * Get signature of the session - * - * @return signature of the session - */ - public SessionSignature getSignature() { - return signature; - } - - /** - * Set signature of the session - * - * @param signature signature of the session - */ - public void setSignature(SessionSignature signature) { - this.signature = signature; - } - - /** - * Get certificate of the session - * - * @return certificate of the session - */ - public SessionCertificate getCert() { - return cert; - } - - /** - * Set certificate of the session - * - * @param cert certificate of the session - */ - public void setCert(SessionCertificate cert) { - this.cert = cert; - } - - /** - * Get ignored properties provided in the session request. - * - * @return ignored properties - */ - public String[] getIgnoredProperties() { - return ignoredProperties; - } - - /** - * Set ignored properties provided in the session request. - * - * @param ignoredProperties ignored properties - */ - public void setIgnoredProperties(String[] ignoredProperties) { - this.ignoredProperties = ignoredProperties; - } - - /** - * Gets the interaction type used in the session - * - * @return the interaction type used in session - */ - public String getInteractionTypeUsed() { - return interactionTypeUsed; - } - - /** - * Sets the interaction type used in the session - * - * @param interactionTypeUsed the interaction type used in session - */ - public void setInteractionTypeUsed(String interactionTypeUsed) { - this.interactionTypeUsed = interactionTypeUsed; - } - - /** - * Gets the IP address of the device used in the session - * - * @return the device IP address - */ - public String getDeviceIpAddress() { - return deviceIpAddress; - } - - /** - * Sets the IP address of the device used in the session - * - * @param deviceIpAddress the device IP address - */ - public void setDeviceIpAddress(String deviceIpAddress) { - this.deviceIpAddress = deviceIpAddress; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Represents response for active session query. + *

+ * state - Required. Current state of the session, e.g. "RUNNING", "COMPLETE"> + * result - Required if state is "COMPLETE". Details about how session ended. + * signatureProtocol - Required if end result is OK. Signature protocol used, e.g. "ACSP_V2" or "RAW_DIGEST_SIGNATURE". + * signature - Required if end result is OK. Signature data containing the actual signature and related information. + * cert - Required if end result is OK. Signer's certificate data. + * ignoredProperties - properties that were ignored from the session request. + * interactionTypeUsed - Required if end result is OK. Interaction type that was used in the session. + * deviceIpAddress - IP address of the device used in the session. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionStatus implements Serializable { + + private String state; + private SessionResult result; + private String signatureProtocol; + private SessionSignature signature; + private SessionCertificate cert; + private String[] ignoredProperties; + private String interactionTypeUsed; + private String deviceIpAddress; + + /** + * Get state of the session + * + * @return state of the session + */ + public String getState() { + return state; + } + + /** + * Set state of the session + * + * @param state state of the session + */ + public void setState(String state) { + this.state = state; + } + + /** + * Get result of the session + * + * @return result of the session + */ + public SessionResult getResult() { + return result; + } + + /** + * Set result of the session + * + * @param result result of the session + */ + public void setResult(SessionResult result) { + this.result = result; + } + + /** + * Get signature protocol used + * + * @return signature protocol used + */ + public String getSignatureProtocol() { + return signatureProtocol; + } + + /** + * Sets the signature protocol used + * + * @param signatureProtocol signature protocol used + */ + public void setSignatureProtocol(String signatureProtocol) { + this.signatureProtocol = signatureProtocol; + } + + /** + * Get signature of the session + * + * @return signature of the session + */ + public SessionSignature getSignature() { + return signature; + } + + /** + * Set signature of the session + * + * @param signature signature of the session + */ + public void setSignature(SessionSignature signature) { + this.signature = signature; + } + + /** + * Get certificate of the session + * + * @return certificate of the session + */ + public SessionCertificate getCert() { + return cert; + } + + /** + * Set certificate of the session + * + * @param cert certificate of the session + */ + public void setCert(SessionCertificate cert) { + this.cert = cert; + } + + /** + * Get ignored properties provided in the session request. + * + * @return ignored properties + */ + public String[] getIgnoredProperties() { + return ignoredProperties; + } + + /** + * Set ignored properties provided in the session request. + * + * @param ignoredProperties ignored properties + */ + public void setIgnoredProperties(String[] ignoredProperties) { + this.ignoredProperties = ignoredProperties; + } + + /** + * Gets the interaction type used in the session + * + * @return the interaction type used in session + */ + public String getInteractionTypeUsed() { + return interactionTypeUsed; + } + + /** + * Sets the interaction type used in the session + * + * @param interactionTypeUsed the interaction type used in session + */ + public void setInteractionTypeUsed(String interactionTypeUsed) { + this.interactionTypeUsed = interactionTypeUsed; + } + + /** + * Gets the IP address of the device used in the session + * + * @return the device IP address + */ + public String getDeviceIpAddress() { + return deviceIpAddress; + } + + /** + * Sets the IP address of the device used in the session + * + * @param deviceIpAddress the device IP address + */ + public void setDeviceIpAddress(String deviceIpAddress) { + this.deviceIpAddress = deviceIpAddress; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java index 1b2677bf..faa46c19 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SessionStatusRequest.java @@ -1,105 +1,105 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -/** - * Represents request to query session status. - *

- * sessionId - the session ID to query status for. - * responseSocketOpenTimeUnit - time unit of how much time a network request socket should be kept open. - * responseSocketOpenTimeValue - time value of how much time a network request socket should be kept opn. - */ -public class SessionStatusRequest implements Serializable { - - private final String sessionId; - private TimeUnit responseSocketOpenTimeUnit; - private long responseSocketOpenTimeValue; - - /** - * Constructs a new SessionStatusRequest with the specified session ID. - * - * @param sessionId the session ID to query status for. - */ - public SessionStatusRequest(String sessionId) { - this.sessionId = sessionId; - } - - /** - * Gets the session ID. - * - * @return the session ID. - */ - public String getSessionId() { - return sessionId; - } - - /** - * Request long poll timeout value. If not provided, a default is used. - *

- * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires - * set by this parameter. - *

- * Caller can tune the request parameters inside the bounds set by service operator. - * - * @param timeUnit time unit of how much time a network request socket should be kept open. - * @param timeValue time value of how much time a network request socket should be kept open. - */ - public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { - responseSocketOpenTimeUnit = timeUnit; - responseSocketOpenTimeValue = timeValue; - } - - /** - * Gets whether response socket open time is set. - * - * @return true if response socket open time is set, false otherwise. - */ - public boolean isResponseSocketOpenTimeSet() { - return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; - } - - /** - * Gets response socket open time unit. - * - * @return response socket open time unit. - */ - public TimeUnit getResponseSocketOpenTimeUnit() { - return responseSocketOpenTimeUnit; - } - - /** - * Gets response socket open time value. - * - * @return response socket open time value. - */ - public long getResponseSocketOpenTimeValue() { - return responseSocketOpenTimeValue; - } -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +/** + * Represents request to query session status. + *

+ * sessionId - the session ID to query status for. + * responseSocketOpenTimeUnit - time unit of how much time a network request socket should be kept open. + * responseSocketOpenTimeValue - time value of how much time a network request socket should be kept opn. + */ +public class SessionStatusRequest implements Serializable { + + private final String sessionId; + private TimeUnit responseSocketOpenTimeUnit; + private long responseSocketOpenTimeValue; + + /** + * Constructs a new SessionStatusRequest with the specified session ID. + * + * @param sessionId the session ID to query status for. + */ + public SessionStatusRequest(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Gets the session ID. + * + * @return the session ID. + */ + public String getSessionId() { + return sessionId; + } + + /** + * Request long poll timeout value. If not provided, a default is used. + *

+ * This parameter is used for a long poll method, meaning the request method might not return until a timeout expires + * set by this parameter. + *

+ * Caller can tune the request parameters inside the bounds set by service operator. + * + * @param timeUnit time unit of how much time a network request socket should be kept open. + * @param timeValue time value of how much time a network request socket should be kept open. + */ + public void setResponseSocketOpenTime(TimeUnit timeUnit, long timeValue) { + responseSocketOpenTimeUnit = timeUnit; + responseSocketOpenTimeValue = timeValue; + } + + /** + * Gets whether response socket open time is set. + * + * @return true if response socket open time is set, false otherwise. + */ + public boolean isResponseSocketOpenTimeSet() { + return responseSocketOpenTimeUnit != null && responseSocketOpenTimeValue > 0; + } + + /** + * Gets response socket open time unit. + * + * @return response socket open time unit. + */ + public TimeUnit getResponseSocketOpenTimeUnit() { + return responseSocketOpenTimeUnit; + } + + /** + * Gets response socket open time value. + * + * @return response socket open time value. + */ + public long getResponseSocketOpenTimeValue() { + return responseSocketOpenTimeValue; + } +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java index 5fb57ff6..97ce547e 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java +++ b/src/main/java/ee/sk/smartid/rest/dao/SignatureAlgorithmParameters.java @@ -1,38 +1,38 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -/** - * Parameters for signature algorithm - * - * @param hashAlgorithm Required. The hash algorithm. - * Supported values are SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 - */ -public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +/** + * Parameters for signature algorithm + * + * @param hashAlgorithm Required. The hash algorithm. + * Supported values are SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512 + */ +public record SignatureAlgorithmParameters(String hashAlgorithm) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java index c066e873..db9ff91d 100644 --- a/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java +++ b/src/main/java/ee/sk/smartid/rest/dao/VerificationCode.java @@ -1,41 +1,41 @@ -package ee.sk.smartid.rest.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.Serializable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Verification code details - * - * @param type Required. Verification code type - * @param value Required. Verification code value - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record VerificationCode(String type, String value) implements Serializable { -} +package ee.sk.smartid.rest.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Verification code details + * + * @param type Required. Verification code type + * @param value Required. Verification code value + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record VerificationCode(String type, String value) implements Serializable { +} diff --git a/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java index a93ee3e0..b12e1fc2 100644 --- a/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java +++ b/src/main/java/ee/sk/smartid/util/CallbackUrlUtil.java @@ -1,91 +1,91 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Base64; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; -import ee.sk.smartid.exception.SessionSecretMismatchException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import jakarta.ws.rs.core.UriBuilder; - -/** - * Utility class for callback URL query parameter related operations. - */ -public final class CallbackUrlUtil { - - private CallbackUrlUtil() { - } - - /** - * Creates a callback URL by appending a random URL-safe token as a query parameter to the provided base URL. - * - * @param baseUrl the URL to which the token will be appended as a query parameter - * @return a {@link CallbackUrl} containing the full callback URL and the generated token - */ - public static CallbackUrl createCallbackUrl(String baseUrl) { - if (StringUtil.isEmpty(baseUrl)) { - throw new SmartIdClientException("Parameter for 'baseUrl' cannot be empty"); - } - String urlToken = UrlSafeTokenGenerator.random(); - return new CallbackUrl(UriBuilder.fromUri(baseUrl).queryParam("value", urlToken).build(), urlToken); - } - - /** - * Validates that the session secret digest from the callback URL matches the calculated digest of the provided session secret. - * - * @param sessionSecretDigest the session secret digest received in the callback URL - * @param sessionSecret the original session secret from the session initialization response - * @throws SmartIdClientException when any input parameters are empty - * @throws SessionSecretMismatchException when the session secrets do not match - */ - public static void validateSessionSecretDigest(String sessionSecretDigest, String sessionSecret) { - if (StringUtil.isEmpty(sessionSecretDigest)) { - throw new SmartIdClientException("Parameter for 'sessionSecretDigest' cannot be empty"); - } - if (StringUtil.isEmpty(sessionSecret)) { - throw new SmartIdClientException("Parameter for 'sessionSecret' cannot be empty"); - } - String calculatedSessionSecret = calculateDigest(sessionSecret); - if (!sessionSecretDigest.equals(calculatedSessionSecret)) { - throw new SessionSecretMismatchException("Session secret digest from callback does not match calculated session secret digest"); - } - } - - private static String calculateDigest(String sessionSecret) { - try { - byte[] decodedSessionSecret = Base64.getDecoder().decode(sessionSecret); - byte[] sessionSecretDigest = DigestCalculator.calculateDigest(decodedSessionSecret, HashAlgorithm.SHA_256); - return Base64.getUrlEncoder().withoutPadding().encodeToString(sessionSecretDigest); - } catch (IllegalArgumentException ex) { - throw new SmartIdClientException("Parameter 'sessionSecret' is not Base64-encoded value", ex); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Base64; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.common.devicelink.UrlSafeTokenGenerator; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import jakarta.ws.rs.core.UriBuilder; + +/** + * Utility class for callback URL query parameter related operations. + */ +public final class CallbackUrlUtil { + + private CallbackUrlUtil() { + } + + /** + * Creates a callback URL by appending a random URL-safe token as a query parameter to the provided base URL. + * + * @param baseUrl the URL to which the token will be appended as a query parameter + * @return a {@link CallbackUrl} containing the full callback URL and the generated token + */ + public static CallbackUrl createCallbackUrl(String baseUrl) { + if (StringUtil.isEmpty(baseUrl)) { + throw new SmartIdClientException("Parameter for 'baseUrl' cannot be empty"); + } + String urlToken = UrlSafeTokenGenerator.random(); + return new CallbackUrl(UriBuilder.fromUri(baseUrl).queryParam("value", urlToken).build(), urlToken); + } + + /** + * Validates that the session secret digest from the callback URL matches the calculated digest of the provided session secret. + * + * @param sessionSecretDigest the session secret digest received in the callback URL + * @param sessionSecret the original session secret from the session initialization response + * @throws SmartIdClientException when any input parameters are empty + * @throws SessionSecretMismatchException when the session secrets do not match + */ + public static void validateSessionSecretDigest(String sessionSecretDigest, String sessionSecret) { + if (StringUtil.isEmpty(sessionSecretDigest)) { + throw new SmartIdClientException("Parameter for 'sessionSecretDigest' cannot be empty"); + } + if (StringUtil.isEmpty(sessionSecret)) { + throw new SmartIdClientException("Parameter for 'sessionSecret' cannot be empty"); + } + String calculatedSessionSecret = calculateDigest(sessionSecret); + if (!sessionSecretDigest.equals(calculatedSessionSecret)) { + throw new SessionSecretMismatchException("Session secret digest from callback does not match calculated session secret digest"); + } + } + + private static String calculateDigest(String sessionSecret) { + try { + byte[] decodedSessionSecret = Base64.getDecoder().decode(sessionSecret); + byte[] sessionSecretDigest = DigestCalculator.calculateDigest(decodedSessionSecret, HashAlgorithm.SHA_256); + return Base64.getUrlEncoder().withoutPadding().encodeToString(sessionSecretDigest); + } catch (IllegalArgumentException ex) { + throw new SmartIdClientException("Parameter 'sessionSecret' is not Base64-encoded value", ex); + } + } +} diff --git a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java index f1fcf869..c946a88c 100644 --- a/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java +++ b/src/main/java/ee/sk/smartid/util/CertificateAttributeUtil.java @@ -1,203 +1,203 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.security.cert.X509Certificate; -import java.text.ParseException; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.ASN1GeneralizedTime; -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.ASN1OctetString; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.DEROctetString; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.DLSet; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -/** - * Utility class for extracting attributes from X.509 certificates. - */ -public final class CertificateAttributeUtil { - - private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); - - private static final String CERTIFICATE_POLICY_OID = "2.5.29.32"; - private static final int KEY_USAGE_NON_REPUDIATION_INDEX = 1; - - private CertificateAttributeUtil() { - } - - /** - * Get Date-of-birth (DoB) from a specific certificate header (if present). - *

- * NB! This attribute may be present on some newer certificates (since ~ May 2021) but not all. - * - * @param x509Certificate Certificate to read the date-of-birth attribute from - * @return Person date of birth or null if this attribute is not set. - * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. - */ - public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { - Optional dateOfBirth = getDateOfBirthCertificateAttribute(x509Certificate); - - return dateOfBirth.map(date -> date.toInstant().atZone(ZoneOffset.UTC).toLocalDate()).orElse(null); - } - - /** - * Get value of attribute in X.500 principal. - * - * @param distinguishedName X.500 distinguished name using the format defined in RFC 2253. - * @param oid Object Identifier (OID) of the attribute to extract - * @return Attribute value - */ - public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { - var x500name = new X500Name(distinguishedName); - RDN[] rdns = x500name.getRDNs(oid); - if (rdns.length == 0) { - return Optional.empty(); - } - return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); - } - - /** - * Extracts certificate policy OID from the given X.509 certificate. - * - * @param certificate the X.509 certificate from which to extract the policy OIDs - * @return a set of certificate policy OIDs as strings; an empty set if no policies are found - * @throws SmartIdClientException if there is an error parsing the certificate policies - */ - public static Set getCertificatePolicy(X509Certificate certificate) { - Set result = new HashSet<>(); - byte[] extensionValue = certificate.getExtensionValue(CERTIFICATE_POLICY_OID); - if (extensionValue == null) { - return result; - } - try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { - ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); - try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { - CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); - for (PolicyInformation pi : policies.getPolicyInformation()) { - result.add(pi.getPolicyIdentifier().getId()); - } - } - } catch (IOException ex) { - throw new SmartIdClientException("Unable to parse certificate policies", ex); - } - return result; - } - - /** - * Checks if the certificate has KeyUsage extension with Non-Repudiation bit set - *

- * This method can be used to check if a certificate is valid for signing in case the certificate profile - * requires that Non-Repudiation bit must be set in KeyUsage extension. - * - * @param certificate the X.509 certificate to check - * @return true if the certificate does not have KeyUsage extension or does not have Non-Repudiation bit set; false otherwise - */ - public static boolean hasNonRepudiationKeyUsage(X509Certificate certificate) { - boolean[] keyUsage = certificate.getKeyUsage(); - return keyUsage != null && keyUsage.length > 1 && keyUsage[KEY_USAGE_NON_REPUDIATION_INDEX]; - } - - private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { - try { - return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); - } catch (IOException | ClassCastException e) { - logger.info("Could not extract date-of-birth from certificate attribute. It seems the attribute does not exist in certificate."); - } catch (ParseException e) { - logger.warn("Date of birth field existed in certificate but failed to parse the value"); - } - return Optional.empty(); - } - - private static Date getDateOfBirthFromAttributeInternal(X509Certificate x509Certificate) throws IOException, ParseException { - byte[] extensionValue = x509Certificate.getExtensionValue(Extension.subjectDirectoryAttributes.getId()); - - if (extensionValue == null) { - logger.debug("subjectDirectoryAttributes field (that carries date-of-birth value) not found from certificate"); - return null; - } - - DEROctetString derOctetString = toDEROctetString(extensionValue); - DLSequence sequence = toDLSequence(derOctetString.getOctets()); - Enumeration objects = ((DLSequence) sequence.getObjectAt(0)).getObjects(); - - while (objects.hasMoreElements()) { - Object param = objects.nextElement(); - - if (param instanceof ASN1ObjectIdentifier id) { - if (id.equals(BCStyle.DATE_OF_BIRTH) && objects.hasMoreElements()) { - Object nextElement = objects.nextElement(); - - DLSet x = ((DLSet) nextElement); - ASN1Encodable objectAt2 = x.getObjectAt(0); - - ASN1GeneralizedTime time = (ASN1GeneralizedTime) objectAt2; - return time.getDate(); - } - } - } - return null; - } - - private static DEROctetString toDEROctetString(byte[] data) throws IOException { - return (DEROctetString) toDerObject(data); - } - - private static DLSequence toDLSequence(byte[] data) throws IOException { - return (DLSequence) toDerObject(data); - } - - private static ASN1Primitive toDerObject(byte[] data) throws IOException { - ByteArrayInputStream inStream = new ByteArrayInputStream(data); - ASN1InputStream asnInputStream = new ASN1InputStream(inStream); - - return asnInputStream.readObject(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1GeneralizedTime; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.DLSet; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +/** + * Utility class for extracting attributes from X.509 certificates. + */ +public final class CertificateAttributeUtil { + + private static final Logger logger = LoggerFactory.getLogger(CertificateAttributeUtil.class); + + private static final String CERTIFICATE_POLICY_OID = "2.5.29.32"; + private static final int KEY_USAGE_NON_REPUDIATION_INDEX = 1; + + private CertificateAttributeUtil() { + } + + /** + * Get Date-of-birth (DoB) from a specific certificate header (if present). + *

+ * NB! This attribute may be present on some newer certificates (since ~ May 2021) but not all. + * + * @param x509Certificate Certificate to read the date-of-birth attribute from + * @return Person date of birth or null if this attribute is not set. + * @see NationalIdentityNumberUtil#getDateOfBirth(AuthenticationIdentity) for fallback. + */ + public static LocalDate getDateOfBirth(X509Certificate x509Certificate) { + Optional dateOfBirth = getDateOfBirthCertificateAttribute(x509Certificate); + + return dateOfBirth.map(date -> date.toInstant().atZone(ZoneOffset.UTC).toLocalDate()).orElse(null); + } + + /** + * Get value of attribute in X.500 principal. + * + * @param distinguishedName X.500 distinguished name using the format defined in RFC 2253. + * @param oid Object Identifier (OID) of the attribute to extract + * @return Attribute value + */ + public static Optional getAttributeValue(String distinguishedName, ASN1ObjectIdentifier oid) { + var x500name = new X500Name(distinguishedName); + RDN[] rdns = x500name.getRDNs(oid); + if (rdns.length == 0) { + return Optional.empty(); + } + return Optional.of(IETFUtils.valueToString(rdns[0].getFirst().getValue())); + } + + /** + * Extracts certificate policy OID from the given X.509 certificate. + * + * @param certificate the X.509 certificate from which to extract the policy OIDs + * @return a set of certificate policy OIDs as strings; an empty set if no policies are found + * @throws SmartIdClientException if there is an error parsing the certificate policies + */ + public static Set getCertificatePolicy(X509Certificate certificate) { + Set result = new HashSet<>(); + byte[] extensionValue = certificate.getExtensionValue(CERTIFICATE_POLICY_OID); + if (extensionValue == null) { + return result; + } + try (ASN1InputStream ais1 = new ASN1InputStream(extensionValue)) { + ASN1OctetString octet = (ASN1OctetString) ais1.readObject(); + try (ASN1InputStream ais2 = new ASN1InputStream(octet.getOctets())) { + CertificatePolicies policies = CertificatePolicies.getInstance(ais2.readObject()); + for (PolicyInformation pi : policies.getPolicyInformation()) { + result.add(pi.getPolicyIdentifier().getId()); + } + } + } catch (IOException ex) { + throw new SmartIdClientException("Unable to parse certificate policies", ex); + } + return result; + } + + /** + * Checks if the certificate has KeyUsage extension with Non-Repudiation bit set + *

+ * This method can be used to check if a certificate is valid for signing in case the certificate profile + * requires that Non-Repudiation bit must be set in KeyUsage extension. + * + * @param certificate the X.509 certificate to check + * @return true if the certificate does not have KeyUsage extension or does not have Non-Repudiation bit set; false otherwise + */ + public static boolean hasNonRepudiationKeyUsage(X509Certificate certificate) { + boolean[] keyUsage = certificate.getKeyUsage(); + return keyUsage != null && keyUsage.length > 1 && keyUsage[KEY_USAGE_NON_REPUDIATION_INDEX]; + } + + private static Optional getDateOfBirthCertificateAttribute(X509Certificate x509Certificate) { + try { + return Optional.ofNullable(getDateOfBirthFromAttributeInternal(x509Certificate)); + } catch (IOException | ClassCastException e) { + logger.info("Could not extract date-of-birth from certificate attribute. It seems the attribute does not exist in certificate."); + } catch (ParseException e) { + logger.warn("Date of birth field existed in certificate but failed to parse the value"); + } + return Optional.empty(); + } + + private static Date getDateOfBirthFromAttributeInternal(X509Certificate x509Certificate) throws IOException, ParseException { + byte[] extensionValue = x509Certificate.getExtensionValue(Extension.subjectDirectoryAttributes.getId()); + + if (extensionValue == null) { + logger.debug("subjectDirectoryAttributes field (that carries date-of-birth value) not found from certificate"); + return null; + } + + DEROctetString derOctetString = toDEROctetString(extensionValue); + DLSequence sequence = toDLSequence(derOctetString.getOctets()); + Enumeration objects = ((DLSequence) sequence.getObjectAt(0)).getObjects(); + + while (objects.hasMoreElements()) { + Object param = objects.nextElement(); + + if (param instanceof ASN1ObjectIdentifier id) { + if (id.equals(BCStyle.DATE_OF_BIRTH) && objects.hasMoreElements()) { + Object nextElement = objects.nextElement(); + + DLSet x = ((DLSet) nextElement); + ASN1Encodable objectAt2 = x.getObjectAt(0); + + ASN1GeneralizedTime time = (ASN1GeneralizedTime) objectAt2; + return time.getDate(); + } + } + } + return null; + } + + private static DEROctetString toDEROctetString(byte[] data) throws IOException { + return (DEROctetString) toDerObject(data); + } + + private static DLSequence toDLSequence(byte[] data) throws IOException { + return (DLSequence) toDerObject(data); + } + + private static ASN1Primitive toDerObject(byte[] data) throws IOException { + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + ASN1InputStream asnInputStream = new ASN1InputStream(inStream); + + return asnInputStream.readObject(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/InteractionUtil.java b/src/main/java/ee/sk/smartid/util/InteractionUtil.java index 3d780327..2ba91cbd 100644 --- a/src/main/java/ee/sk/smartid/util/InteractionUtil.java +++ b/src/main/java/ee/sk/smartid/util/InteractionUtil.java @@ -1,88 +1,88 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; -import java.util.Objects; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.common.SmartIdInteraction; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.Interaction; - -/** - * Utility for interactions related actions - */ -public class InteractionUtil { - - private static final ObjectMapper mapper = new ObjectMapper(); - - private InteractionUtil() { - } - - /** - * Encodes list of interactions to Base64-encoded string - * - * @param interactions list of interactions - * @return base64 encoded string - * @throws SmartIdClientException if unable to encode interactions - */ - public static String encodeToBase64(List interactions) { - try { - String json = mapper.writeValueAsString(interactions); - return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException ex) { - throw new SmartIdClientException("Unable to encode interactions to Base64", ex); - } - } - - /** - * Calculates SHA-256 digest of the interactions and encodes it to Base64 - * - * @param interactions interactions string - * @return base64 encoded SHA-256 digest - */ - public static String calculateDigest(String interactions){ - byte[] digest = DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - return Base64.getEncoder().encodeToString(digest); - } - - /** - * Checks if the list of interactions is empty or contains only null values - * - * @param interactions list of interactions - * @return true if the list is empty or contains only null values, false otherwise - */ - public static boolean isEmpty(List interactions) { - return interactions == null || interactions.stream().filter(Objects::nonNull).toList().isEmpty(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.common.SmartIdInteraction; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.Interaction; + +/** + * Utility for interactions related actions + */ +public class InteractionUtil { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private InteractionUtil() { + } + + /** + * Encodes list of interactions to Base64-encoded string + * + * @param interactions list of interactions + * @return base64 encoded string + * @throws SmartIdClientException if unable to encode interactions + */ + public static String encodeToBase64(List interactions) { + try { + String json = mapper.writeValueAsString(interactions); + return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + } catch (JsonProcessingException ex) { + throw new SmartIdClientException("Unable to encode interactions to Base64", ex); + } + } + + /** + * Calculates SHA-256 digest of the interactions and encodes it to Base64 + * + * @param interactions interactions string + * @return base64 encoded SHA-256 digest + */ + public static String calculateDigest(String interactions){ + byte[] digest = DigestCalculator.calculateDigest(interactions.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + return Base64.getEncoder().encodeToString(digest); + } + + /** + * Checks if the list of interactions is empty or contains only null values + * + * @param interactions list of interactions + * @return true if the list is empty or contains only null values, false otherwise + */ + public static boolean isEmpty(List interactions) { + return interactions == null || interactions.stream().filter(Objects::nonNull).toList().isEmpty(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java index eb59d3f0..64ac36e7 100644 --- a/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java +++ b/src/main/java/ee/sk/smartid/util/NationalIdentityNumberUtil.java @@ -1,142 +1,142 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.format.ResolverStyle; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -/** - * Utility class for handling national identity numbers (personal codes). - */ -public class NationalIdentityNumberUtil { - - private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); - - private static final DateTimeFormatter DATE_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuuMMdd") - .withResolverStyle(ResolverStyle.STRICT); - - /** - * Detect date-of-birth from a Baltic national identification number if possible or return null. - *

- * This method always returns the value for all Estonian and Lithuanian national identification numbers. - *

- * It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 - * (starting with "32") do not carry date-of-birth. - *

- * For non-Baltic countries (countries other than Estonia, Latvia or Lithuania) it always returns null - * (even if it would be possible to deduce date of birth from national identity number). - *

- * Newer (but not all) Smart-ID certificates have date-of-birth on a separate attribute. - * It is recommended to use that value if present. - * - * @param authenticationIdentity Authentication identity - * @return DateOfBirth or null if it cannot be detected from personal code - * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) - */ - public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { - String identityNumber = authenticationIdentity.getIdentityNumber(); - - return switch (authenticationIdentity.getCountry().toUpperCase()) { - case "EE", "LT" -> parseEeLtDateOfBirth(identityNumber); - case "LV" -> parseLvDateOfBirth(identityNumber); - default -> null; - }; - } - - /** - * Parses date of birth from Estonian or Lithuanian national identity number. - * - * @param eeOrLtNationalIdentityNumber Estonian or Lithuanian national identity number - * @return Date of birth - * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed - */ - public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { - String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); - - birthDate = switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { - case "1", "2" -> "18" + birthDate; - case "3", "4" -> "19" + birthDate; - case "5", "6" -> "20" + birthDate; - default -> throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); - }; - - try { - return LocalDate.parse(birthDate, DATE_FORMATTER_YYYY_MM_DD); - } catch (DateTimeParseException e) { - throw new UnprocessableSmartIdResponseException("Could not parse birthdate from nationalIdentityNumber=" + eeOrLtNationalIdentityNumber, e); - } - } - - /** - * Parses date of birth from Latvian national identity number if possible. - *

- * Latvian personal codes issued after July 1st 2017 (starting with "32") do not carry date-of-birth and null is returned. - * - * @param lvNationalIdentityNumber Latvian national identity number - * @return Date of birth or null if the personal code does not carry birthdate info - * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed - */ - public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { - String birthDay = lvNationalIdentityNumber.substring(0, 2); - if (isNonParsableLVPersonCodePrefix(birthDay)) { - logger.debug("Person has newer type of Latvian ID-code that does not carry birthdate info"); - return null; - } - - String birthMonth = lvNationalIdentityNumber.substring(2, 4); - String birthYearTwoDigit = lvNationalIdentityNumber.substring(4, 6); - String century = lvNationalIdentityNumber.substring(7, 8); - String birthDateYyyyMmDd = switch (century) { - case "0" -> "18" + (birthYearTwoDigit + birthMonth + birthDay); - case "1" -> "19" + (birthYearTwoDigit + birthMonth + birthDay); - case "2" -> "20" + (birthYearTwoDigit + birthMonth + birthDay); - default -> throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); - }; - - try { - return LocalDate.parse(birthDateYyyyMmDd, DATE_FORMATTER_YYYY_MM_DD); - } catch (DateTimeParseException e) { - throw new UnprocessableSmartIdResponseException("Unable get birthdate from Latvian personal code " + lvNationalIdentityNumber, e); - } - } - - private static boolean isNonParsableLVPersonCodePrefix(String prefix) { - Pattern pattern = Pattern.compile("3[2-9]"); - Matcher matcher = pattern.matcher(prefix); - return matcher.matches(); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +/** + * Utility class for handling national identity numbers (personal codes). + */ +public class NationalIdentityNumberUtil { + + private static final Logger logger = LoggerFactory.getLogger(NationalIdentityNumberUtil.class); + + private static final DateTimeFormatter DATE_FORMATTER_YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuuMMdd") + .withResolverStyle(ResolverStyle.STRICT); + + /** + * Detect date-of-birth from a Baltic national identification number if possible or return null. + *

+ * This method always returns the value for all Estonian and Lithuanian national identification numbers. + *

+ * It also works for older Latvian personal codes but Latvian personal codes issued after July 1st 2017 + * (starting with "32") do not carry date-of-birth. + *

+ * For non-Baltic countries (countries other than Estonia, Latvia or Lithuania) it always returns null + * (even if it would be possible to deduce date of birth from national identity number). + *

+ * Newer (but not all) Smart-ID certificates have date-of-birth on a separate attribute. + * It is recommended to use that value if present. + * + * @param authenticationIdentity Authentication identity + * @return DateOfBirth or null if it cannot be detected from personal code + * @see CertificateAttributeUtil#getDateOfBirth(java.security.cert.X509Certificate) + */ + public static LocalDate getDateOfBirth(AuthenticationIdentity authenticationIdentity) { + String identityNumber = authenticationIdentity.getIdentityNumber(); + + return switch (authenticationIdentity.getCountry().toUpperCase()) { + case "EE", "LT" -> parseEeLtDateOfBirth(identityNumber); + case "LV" -> parseLvDateOfBirth(identityNumber); + default -> null; + }; + } + + /** + * Parses date of birth from Estonian or Lithuanian national identity number. + * + * @param eeOrLtNationalIdentityNumber Estonian or Lithuanian national identity number + * @return Date of birth + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ + public static LocalDate parseEeLtDateOfBirth(String eeOrLtNationalIdentityNumber) { + String birthDate = eeOrLtNationalIdentityNumber.substring(1, 7); + + birthDate = switch (eeOrLtNationalIdentityNumber.substring(0, 1)) { + case "1", "2" -> "18" + birthDate; + case "3", "4" -> "19" + birthDate; + case "5", "6" -> "20" + birthDate; + default -> throw new RuntimeException("Invalid personal code " + eeOrLtNationalIdentityNumber); + }; + + try { + return LocalDate.parse(birthDate, DATE_FORMATTER_YYYY_MM_DD); + } catch (DateTimeParseException e) { + throw new UnprocessableSmartIdResponseException("Could not parse birthdate from nationalIdentityNumber=" + eeOrLtNationalIdentityNumber, e); + } + } + + /** + * Parses date of birth from Latvian national identity number if possible. + *

+ * Latvian personal codes issued after July 1st 2017 (starting with "32") do not carry date-of-birth and null is returned. + * + * @param lvNationalIdentityNumber Latvian national identity number + * @return Date of birth or null if the personal code does not carry birthdate info + * @throws UnprocessableSmartIdResponseException if the national identity number is invalid or date cannot be parsed + */ + public static LocalDate parseLvDateOfBirth(String lvNationalIdentityNumber) { + String birthDay = lvNationalIdentityNumber.substring(0, 2); + if (isNonParsableLVPersonCodePrefix(birthDay)) { + logger.debug("Person has newer type of Latvian ID-code that does not carry birthdate info"); + return null; + } + + String birthMonth = lvNationalIdentityNumber.substring(2, 4); + String birthYearTwoDigit = lvNationalIdentityNumber.substring(4, 6); + String century = lvNationalIdentityNumber.substring(7, 8); + String birthDateYyyyMmDd = switch (century) { + case "0" -> "18" + (birthYearTwoDigit + birthMonth + birthDay); + case "1" -> "19" + (birthYearTwoDigit + birthMonth + birthDay); + case "2" -> "20" + (birthYearTwoDigit + birthMonth + birthDay); + default -> throw new UnprocessableSmartIdResponseException("Invalid personal code: " + lvNationalIdentityNumber); + }; + + try { + return LocalDate.parse(birthDateYyyyMmDd, DATE_FORMATTER_YYYY_MM_DD); + } catch (DateTimeParseException e) { + throw new UnprocessableSmartIdResponseException("Unable get birthdate from Latvian personal code " + lvNationalIdentityNumber, e); + } + } + + private static boolean isNonParsableLVPersonCodePrefix(String prefix) { + Pattern pattern = Pattern.compile("3[2-9]"); + Matcher matcher = pattern.matcher(prefix); + return matcher.matches(); + } +} diff --git a/src/main/java/ee/sk/smartid/util/SetUtil.java b/src/main/java/ee/sk/smartid/util/SetUtil.java index fe763903..9ea2a928 100644 --- a/src/main/java/ee/sk/smartid/util/SetUtil.java +++ b/src/main/java/ee/sk/smartid/util/SetUtil.java @@ -1,55 +1,55 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Arrays; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Utility class for Set operations. - */ -public final class SetUtil { - - private SetUtil() { - } - - /** - * Converts an array to a Set, filtering out null or empty values. - * - * @param array array to be converted - * @return a set of non-null, non-empty trimmed strings - */ - public static Set toSet(String[] array) { - return Arrays.stream(array) - .filter(Objects::nonNull) - .map(String::trim) - .filter(StringUtil::isNotEmpty) - .collect(Collectors.toSet()); - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for Set operations. + */ +public final class SetUtil { + + private SetUtil() { + } + + /** + * Converts an array to a Set, filtering out null or empty values. + * + * @param array array to be converted + * @return a set of non-null, non-empty trimmed strings + */ + public static Set toSet(String[] array) { + return Arrays.stream(array) + .filter(Objects::nonNull) + .map(String::trim) + .filter(StringUtil::isNotEmpty) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/ee/sk/smartid/util/StringUtil.java b/src/main/java/ee/sk/smartid/util/StringUtil.java index 0b0fb943..6274c4e8 100644 --- a/src/main/java/ee/sk/smartid/util/StringUtil.java +++ b/src/main/java/ee/sk/smartid/util/StringUtil.java @@ -1,66 +1,66 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -/** - * Utility class to handle string operations - */ -public final class StringUtil { - - private StringUtil() { - } - - /** - * Checks that given CharSequence is not null and not empty - * - * @param cs the CharSequence to check - * @return true if the CharSequence is not null and not empty, false otherwise - */ - public static boolean isNotEmpty(final CharSequence cs) { - return cs != null && !cs.isEmpty(); - } - - /** - * Checks that given CharSequence is null or empty - * - * @param cs the CharSequence to check - * @return true if the CharSequence is null or empty, false otherwise - */ - public static boolean isEmpty(final CharSequence cs) { - return cs == null || cs.isEmpty(); - } - - /** - * Checks that given string is not null and not empty - * - * @param input the value to check - * @return String if the input is not null and not empty, empty string otherwise - */ - public static String orEmpty(String input) { - return input == null ? "" : input; - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +/** + * Utility class to handle string operations + */ +public final class StringUtil { + + private StringUtil() { + } + + /** + * Checks that given CharSequence is not null and not empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is not null and not empty, false otherwise + */ + public static boolean isNotEmpty(final CharSequence cs) { + return cs != null && !cs.isEmpty(); + } + + /** + * Checks that given CharSequence is null or empty + * + * @param cs the CharSequence to check + * @return true if the CharSequence is null or empty, false otherwise + */ + public static boolean isEmpty(final CharSequence cs) { + return cs == null || cs.isEmpty(); + } + + /** + * Checks that given string is not null and not empty + * + * @param input the value to check + * @return String if the input is not null and not empty, empty string otherwise + */ + public static String orEmpty(String input) { + return input == null ? "" : input; + } +} diff --git a/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt b/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt index 62466c24..aba475f6 100644 --- a/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt +++ b/src/main/resources/trusted_certificates/EID-SK_2016.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIG4jCCBcqgAwIBAgIQO4A6a2nBKoxXxVAFMRvE2jANBgkqhkiG9w0BAQwFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MjEwOVoYDzIwMzAxMjE3 -MjM1OTU5WjBgMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFDASBgNVBAMMC0VJ -RC1TSyAyMDE2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7XWFN0j -1CFoGIuVe9xRezEnA0Tk3vmvIpvURX+y7Z5DJsfub2mtpSLtbhXjAeynq9QV78zj -gQ73pNVGh+GQ6oPG7HF8KIlZuIYsf1+gBxPxNiLa0+sCWxa6p4HQbgdgYRVGod4I -Qbib9KbOki3wjCG5WiWh1SP9qcuTZVY+9zawkSMf65Px/Y4ChjtNFtY66MEvsPCh -lHHfsBNiUbtZ68jJNYCECjtkm0vxz2iiSXB2WRIv3/hTrRgMJ2CNMyFjRQoGQlpH -010+fcisObKeyPwA8kI22Oto9MzLw7KsY524OD3B1L5MExYxHD916XIEHT/9gBP2 -Zn8qZu/BllKdSIapOIJW9ZEw+3w5UOU6LT3tTSbAzeQAnD3eCABPifYwHYC0lmKs -PpQJqtx0Q3Jbm3BGReYiZ9KuK36nF/G78YjhM+yioERr2B/cKf31j0W/GuGvyHak -bokwy7nsbL30sTuRLR70Oqi5UBMy4e8J2CduR3R3NJw5UqpScJIchngsLAx+WsyC -0w38AmMewMBcnlp/QbakKo52HrsYRR1m+NhCVDBy45Lzl8I0/OGd9Ikdg1h7T7SI -guZVpyzys8E0yfrcS5YMEd9hMqVPr7rszXCzbxyw0tVIk8QLMw/lI+XE1Oi7Skgz -A2i5Vpa6i2K0ard6GPHzRqGPTkjc5Z4DzZMCAwEAAaOCAn8wggJ7MB8GA1UdIwQY -MBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBScCagHhww9rC6H/KCu -0vtlSYgo+zAOBgNVHQ8BAf8EBAMCAQYwgcQGA1UdIASBvDCBuTA8BgcEAIvsQAEC -MDEwLwYIKwYBBQUHAgEWI2h0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0v -Q1BTMDwGBwQAi+xAAQAwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUv -cmVwb3NpdG9vcml1bS9DUFMwOwYGBACPegECMDEwLwYIKwYBBQUHAgEWI2h0dHBz -Oi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMBIGA1UdEwEB/wQIMAYBAf8C -AQAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB8Bggr -BgEFBQcBAQRwMG4wIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3NwLnNrLmVlL0NBMEoG -CCsGAQUFBzAChj5odHRwOi8vd3d3LnNrLmVlL2NlcnRzL0VFX0NlcnRpZmljYXRp -b25fQ2VudHJlX1Jvb3RfQ0EuZGVyLmNydDBBBgNVHR4EOjA4oTYwBIICIiIwCocI -AAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJQYI -KwYBBQUHAQMEGTAXMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwPQYDVR0fBDYwNDAy -oDCgLoYsaHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5j -cmwwDQYJKoZIhvcNAQEMBQADggEBAKSIoud5DSfhDU6yp+VrXYL40wi5zFTf19ha -/kO/zzLxZ1hf45VJmSyukMWaWXEqhaLWBZuw5kP78mQ0HyaRUennN0hom/pEiBz6 -cuz9oc+xlmPAZM25ZoaLqa4upP2/+NCWoRTzYkIdc9MEECs5RMBUmyT1G4s8J6n8 -L2M2yYadBMvPGJS3yXxYdc/b3a2foiw3kKa/q1tXAHXZCsuxFVYxXdZt3AwInYHe -mCVKjZg8BaRpvIEXd3AgJwt+9bpV/x0/MouRPNRv0jjWIx1sAlL94hO74WZDMFbZ -VaV6gpG77X2P3dPHKFIRWzjtSQJX4C5n1uvQBxO4ABoMswq0lq0= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG4jCCBcqgAwIBAgIQO4A6a2nBKoxXxVAFMRvE2jANBgkqhkiG9w0BAQwFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MjEwOVoYDzIwMzAxMjE3 +MjM1OTU5WjBgMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFDASBgNVBAMMC0VJ +RC1TSyAyMDE2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7XWFN0j +1CFoGIuVe9xRezEnA0Tk3vmvIpvURX+y7Z5DJsfub2mtpSLtbhXjAeynq9QV78zj +gQ73pNVGh+GQ6oPG7HF8KIlZuIYsf1+gBxPxNiLa0+sCWxa6p4HQbgdgYRVGod4I +Qbib9KbOki3wjCG5WiWh1SP9qcuTZVY+9zawkSMf65Px/Y4ChjtNFtY66MEvsPCh +lHHfsBNiUbtZ68jJNYCECjtkm0vxz2iiSXB2WRIv3/hTrRgMJ2CNMyFjRQoGQlpH +010+fcisObKeyPwA8kI22Oto9MzLw7KsY524OD3B1L5MExYxHD916XIEHT/9gBP2 +Zn8qZu/BllKdSIapOIJW9ZEw+3w5UOU6LT3tTSbAzeQAnD3eCABPifYwHYC0lmKs +PpQJqtx0Q3Jbm3BGReYiZ9KuK36nF/G78YjhM+yioERr2B/cKf31j0W/GuGvyHak +bokwy7nsbL30sTuRLR70Oqi5UBMy4e8J2CduR3R3NJw5UqpScJIchngsLAx+WsyC +0w38AmMewMBcnlp/QbakKo52HrsYRR1m+NhCVDBy45Lzl8I0/OGd9Ikdg1h7T7SI +guZVpyzys8E0yfrcS5YMEd9hMqVPr7rszXCzbxyw0tVIk8QLMw/lI+XE1Oi7Skgz +A2i5Vpa6i2K0ard6GPHzRqGPTkjc5Z4DzZMCAwEAAaOCAn8wggJ7MB8GA1UdIwQY +MBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBScCagHhww9rC6H/KCu +0vtlSYgo+zAOBgNVHQ8BAf8EBAMCAQYwgcQGA1UdIASBvDCBuTA8BgcEAIvsQAEC +MDEwLwYIKwYBBQUHAgEWI2h0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0v +Q1BTMDwGBwQAi+xAAQAwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUv +cmVwb3NpdG9vcml1bS9DUFMwOwYGBACPegECMDEwLwYIKwYBBQUHAgEWI2h0dHBz +Oi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMBIGA1UdEwEB/wQIMAYBAf8C +AQAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB8Bggr +BgEFBQcBAQRwMG4wIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3NwLnNrLmVlL0NBMEoG +CCsGAQUFBzAChj5odHRwOi8vd3d3LnNrLmVlL2NlcnRzL0VFX0NlcnRpZmljYXRp +b25fQ2VudHJlX1Jvb3RfQ0EuZGVyLmNydDBBBgNVHR4EOjA4oTYwBIICIiIwCocI +AAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJQYI +KwYBBQUHAQMEGTAXMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwPQYDVR0fBDYwNDAy +oDCgLoYsaHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5j +cmwwDQYJKoZIhvcNAQEMBQADggEBAKSIoud5DSfhDU6yp+VrXYL40wi5zFTf19ha +/kO/zzLxZ1hf45VJmSyukMWaWXEqhaLWBZuw5kP78mQ0HyaRUennN0hom/pEiBz6 +cuz9oc+xlmPAZM25ZoaLqa4upP2/+NCWoRTzYkIdc9MEECs5RMBUmyT1G4s8J6n8 +L2M2yYadBMvPGJS3yXxYdc/b3a2foiw3kKa/q1tXAHXZCsuxFVYxXdZt3AwInYHe +mCVKjZg8BaRpvIEXd3AgJwt+9bpV/x0/MouRPNRv0jjWIx1sAlL94hO74WZDMFbZ +VaV6gpG77X2P3dPHKFIRWzjtSQJX4C5n1uvQBxO4ABoMswq0lq0= +-----END CERTIFICATE----- diff --git a/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt b/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt index e9cb6d63..b300155e 100644 --- a/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt +++ b/src/main/resources/trusted_certificates/NQ-SK_2016.pem.crt @@ -1,37 +1,37 @@ ------BEGIN CERTIFICATE----- -MIIGYjCCBUqgAwIBAgIQV6nz7KIvDihXxU71YTbgWjANBgkqhkiG9w0BAQwFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MTYzN1oYDzIwMzAxMjE3 -MjM1OTU5WjBfMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy -aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzARBgNVBAMMCk5R -LVNLIDIwMTYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdkRRNDxfO -6oKU9GDrGLNQc41PA+pqDKCEcDhSw1bnkC/nDumg4PawQk8xklyDHr2ShrsFrTo5 -wps5UcgxxTMqb98bmMxQYghqxu5NqqpaZopbbSj+qDYUzrZkXIlVe+HFpUt5ce9W -NpEmeenVAlt4ZaN1/srDfv3NSMmcF2r9XiUIIhDavxQ+QgPy3CrgT0Ja3yw/PLpF -/ajCNQWaGWJHYkgNVzrnrKhKYDhgorc3lSqGfTfhW2Xf5klvBZokPfbhD26csnPe -JjQQQJ2Loot3Z9/QPzfY/Qnqp5hjkvfqjKksX2wAt/UB+Hk4sRG+6Nqa3b+gxqMc -ih1eI/I93Ii6OC7LijhN2k0R9L5+ArgQXhlAQYZGeCAC/unHmpCkiUQrEJq27kst -mzoENnwQnF3mhq81KQGZul/Guw1fsQOolALESEWG6dTP1szaLeba4LYN707b9puR -OVXk1WLoau131KZnIdc/+Ktu2ni4SVL3+qKbJ7+oqIfiFAqlSuCPTKssdFC49m7V -G4bXnrYeA5svUQjCvpANmzXqRs6DmdctKPuXUj+W/gnQNoLOvIEkK30TD/RKd4eh -uzzYj9qirhqBDFg+Ipqh9OByK7aY6f9KZ6qKmKttcPb4R7arBtuQoBlqadcXoGig -o/kr/iXVRabWfGVM73iQo36RZrklrSu5awIDAQABo4ICADCCAfwwHwYDVR0jBBgw -FoAUEvJaPupWHL/NBqzx8SXJqUvUFJkwHQYDVR0OBBYEFHq3hV+h88xBt67p6gZR -CuD5AsisMA4GA1UdDwEB/wQEAwIBBjBGBgNVHSAEPzA9MDsGBgQAj3oBATAxMC8G -CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS -BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD -AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v -b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0 -cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e -BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs -SQEBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9y -eS9jcmxzL2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQCu4HLsEBBpKmXw -agXpFkmEGqTOC/eYWrtwVZNnz/MB+z8c6TyxwW2cDmNwMwMfojXT447rQ/xlai/5 -gjGkRwRE8P5W90h/JkO3rUWG4asrvPAwmkIiUHsHIHDVHCsSmhLNEgPdM4zP88/L -EmV89ZIvUWGjzZYcgBljYeAlK4dCy4/7U14JW9FCwvFjFOyfDcpoYwxbV7Jkbhsw -9J8uzzxjspGCvoq5izeTGuRV+WtV+yy6W/UOnpmYOJ6jxzUoYq6fnQGU+J9CLVxj -jE8Jj25fuWSU3BvPs8cms7RzvvuZvPgQWm8IZt6L5P6EHOzeER3m3nftERhG2OXE -+MHJ+onc ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGYjCCBUqgAwIBAgIQV6nz7KIvDihXxU71YTbgWjANBgkqhkiG9w0BAQwFADB1 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG +CSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE2MDgzMDA5MTYzN1oYDzIwMzAxMjE3 +MjM1OTU5WjBfMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy +aW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzARBgNVBAMMCk5R +LVNLIDIwMTYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDdkRRNDxfO +6oKU9GDrGLNQc41PA+pqDKCEcDhSw1bnkC/nDumg4PawQk8xklyDHr2ShrsFrTo5 +wps5UcgxxTMqb98bmMxQYghqxu5NqqpaZopbbSj+qDYUzrZkXIlVe+HFpUt5ce9W +NpEmeenVAlt4ZaN1/srDfv3NSMmcF2r9XiUIIhDavxQ+QgPy3CrgT0Ja3yw/PLpF +/ajCNQWaGWJHYkgNVzrnrKhKYDhgorc3lSqGfTfhW2Xf5klvBZokPfbhD26csnPe +JjQQQJ2Loot3Z9/QPzfY/Qnqp5hjkvfqjKksX2wAt/UB+Hk4sRG+6Nqa3b+gxqMc +ih1eI/I93Ii6OC7LijhN2k0R9L5+ArgQXhlAQYZGeCAC/unHmpCkiUQrEJq27kst +mzoENnwQnF3mhq81KQGZul/Guw1fsQOolALESEWG6dTP1szaLeba4LYN707b9puR +OVXk1WLoau131KZnIdc/+Ktu2ni4SVL3+qKbJ7+oqIfiFAqlSuCPTKssdFC49m7V +G4bXnrYeA5svUQjCvpANmzXqRs6DmdctKPuXUj+W/gnQNoLOvIEkK30TD/RKd4eh +uzzYj9qirhqBDFg+Ipqh9OByK7aY6f9KZ6qKmKttcPb4R7arBtuQoBlqadcXoGig +o/kr/iXVRabWfGVM73iQo36RZrklrSu5awIDAQABo4ICADCCAfwwHwYDVR0jBBgw +FoAUEvJaPupWHL/NBqzx8SXJqUvUFJkwHQYDVR0OBBYEFHq3hV+h88xBt67p6gZR +CuD5AsisMA4GA1UdDwEB/wQEAwIBBjBGBgNVHSAEPzA9MDsGBgQAj3oBATAxMC8G +CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS +BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD +AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v +b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0 +cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e +BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs +SQEBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9y +eS9jcmxzL2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQCu4HLsEBBpKmXw +agXpFkmEGqTOC/eYWrtwVZNnz/MB+z8c6TyxwW2cDmNwMwMfojXT447rQ/xlai/5 +gjGkRwRE8P5W90h/JkO3rUWG4asrvPAwmkIiUHsHIHDVHCsSmhLNEgPdM4zP88/L +EmV89ZIvUWGjzZYcgBljYeAlK4dCy4/7U14JW9FCwvFjFOyfDcpoYwxbV7Jkbhsw +9J8uzzxjspGCvoq5izeTGuRV+WtV+yy6W/UOnpmYOJ6jxzUoYq6fnQGU+J9CLVxj +jE8Jj25fuWSU3BvPs8cms7RzvvuZvPgQWm8IZt6L5P6EHOzeER3m3nftERhG2OXE ++MHJ+onc +-----END CERTIFICATE----- diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java index 001a92ff..2da25525 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityMapperTest.java @@ -1,54 +1,54 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; - -import org.junit.jupiter.api.Test; - -class AuthenticationIdentityMapperTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - - @Test - void from() { - X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); - AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); - - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("40504040001", authenticationIdentity.getIdentityNumber()); - assertEquals("EE", authenticationIdentity.getCountry()); - - assertEquals(certificate, authenticationIdentity.getAuthCertificate()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class AuthenticationIdentityMapperTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + @Test + void from() { + X509Certificate certificate = CertificateUtil.toX509Certificate(AUTH_CERT); + AuthenticationIdentity authenticationIdentity = AuthenticationIdentityMapper.from(certificate); + + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("40504040001", authenticationIdentity.getIdentityNumber()); + assertEquals("EE", authenticationIdentity.getCountry()); + + assertEquals(certificate, authenticationIdentity.getAuthCertificate()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java index 6406af74..c18f84dd 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationIdentityTest.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; - -public class AuthenticationIdentityTest { - - @Test - public void getIdentityCode() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setIdentityNumber("identityNumber"); - - assertThat(authenticationIdentity.getIdentityCode(), is("identityNumber")); - } - - @Test - public void setIdentityCode() { - AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); - authenticationIdentity.setIdentityCode("identityCode"); - - assertThat(authenticationIdentity.getIdentityNumber(), is("identityCode")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +public class AuthenticationIdentityTest { + + @Test + public void getIdentityCode() { + AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); + authenticationIdentity.setIdentityNumber("identityNumber"); + + assertThat(authenticationIdentity.getIdentityCode(), is("identityNumber")); + } + + @Test + public void setIdentityCode() { + AuthenticationIdentity authenticationIdentity = new AuthenticationIdentity(); + authenticationIdentity.setIdentityCode("identityCode"); + + assertThat(authenticationIdentity.getIdentityNumber(), is("identityCode")); + } +} diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index dfcd6626..a671136a 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -1,837 +1,837 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; - -class AuthenticationResponseMapperImplTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - - private AuthenticationResponseMapper authenticationResponseMapper; - - @BeforeEach - void setUp() { - authenticationResponseMapper = new AuthenticationResponseMapperImpl(); - } - - @Test - void from() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - } - - @ParameterizedTest - @EnumSource(FlowType.class) - void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - sessionSignature.setFlowType(flowType.getDescription()); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); - sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); - sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); - - AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); - - assertEquals("OK", authenticationResponse.getEndResult()); - assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); - assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); - assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); - assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); - assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); - assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); - assertEquals(hashAlgorithm, authenticationResponse.getRsaSsaPssSignatureParameters().getDigestHashAlgorithm()); - assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getRsaSsaPssSignatureParameters().getSaltLength()); - } - - @Test - void from_sessionStatusNull_throwException() { - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); - assertEquals("Parameter 'sessionsStatus' is not provided", exception.getMessage()); - } - - @Nested - class ValidateResult { - - @Test - void from_sessionResultIsNotPresent_throwException() { - var sessionStatus = new SessionStatus(); - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result' is empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_endResultIsNotPresent_throwException(String endResult) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result.endResult' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void from_endResultIsError_throwException(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); - } - - @ParameterizedTest - @NullAndEmptySource - void from_documentNumberIsEmpty_throwException(String documentNumber) { - var sessionResult = toSessionResult(documentNumber); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'result.documentNumber' is empty", exception.getMessage()); - } - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(signatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signatureProtocol' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) - void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol(invalidSignatureProtocol); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signatureProtocol' has unsupported value", exception.getMessage()); - } - - @Nested - class ValidateSignature { - - @Test - void from_signatureIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureValueIsNotProvided_throwException(String signatureValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.value' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\|invalidSignatureValue|", "#1234567890"}) - void from_signatureValueDoesNotMatchThePattern_throwException(String signatureValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.value' does not have Base64-encoded value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_serverRandomIsNotProvided_throwException(String serverRandom) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom(serverRandom); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' is empty", exception.getMessage()); - } - - @Test - void from_serverRandomLengthIsLessThanAllowed_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(23)); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' value length is less than required", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\|YXRsZWFzdDI0Y2hhcmFjdGVycw|", "#YXRsZWFzdDI0Y2hhcmFjdGVycw"}) - void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRandom) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom(serverRandom); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_userChallengeIsEmpty_throwException(String userChallenge) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge(userChallenge); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.userChallenge' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"\\#dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsd", "dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdW="}) - void from_providedUserChallengeDoesNotMatchThePattern_throwException(String userChallenge) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge(userChallenge); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.userChallenge' value does not match required pattern", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_flowTypeNotProvided_throwException(String flowType) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType(flowType); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.flowType' is empty", exception.getMessage()); - } - - @Test - void from_flowTypeNotSupported_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.flowType' has unsupported value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithm); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithm' is empty", exception.getMessage()); - } - - @Test - void from_signatureAlgorithmIsNotSupported_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithm' has unsupported value", exception.getMessage()); - } - - @Nested - class ValidateSignatureAlgorithmParameters { - - @Test - void from_signatureAlgorithmParametersAreMissing_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(null); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "invalid"}) - void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm(invalidHashAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_masGenAlgorithmIsMissing_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(algorithm); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", exception.getMessage()); - } - - @Test - void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("invalid"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_parametersInMaskGenAlgorithmAreMissing_throwException() { - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(null); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_hashAlgorithmInMaskGenAlgorithmParametersIsEmpty_throwException(String hashAlgorithm) { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", exception.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "asdhfasdf"}) - void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String hashAlgorithm) { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); - } - - @Test - void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); - } - - @Test - void from_saltLengthIsMissing_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(null); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty", exception.getMessage()); - } - - @Test - void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(20); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_trailerFieldIsEmpty_throwException(String trailerField) { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField(trailerField); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty", exception.getMessage()); - } - - @Test - void from_trailerFieldValueIsInvalid_throwException() { - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField("invalid"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature(signatureAlgorithmParameters); - var sessionStatus = toSessionStatus(sessionResult, sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); - } - - private static SessionSignature toSessionSignature(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("a".repeat(24)); - sessionSignature.setUserChallenge("a".repeat(43)); - sessionSignature.setFlowType("QR"); - sessionSignature.setSignatureAlgorithm("rsassa-pss"); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - return sessionSignature; - } - } - - private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature) { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - return sessionStatus; - } - - private static SessionMaskGenAlgorithmParameters toMaskGenAlgorithmParameters() { - var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - return maskGenAlgorithmParameters; - } - } - - @Nested - class ValidateCertificate { - - @Test - void from_sessionCertificateIsNotProvided_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert' is missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.value' is empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.certificateLevel' is empty", exception.getMessage()); - } - - @Test - void from_certificateIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); - } - - @Test - void from_certificateLevelIsInvalid_throwException() { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "invalid"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'cert.certificateLevel' has unsupported value", exception.getMessage()); - } - } - - @ParameterizedTest - @NullAndEmptySource - void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUsed) { - var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); - var sessionSignature = toSessionSignature("rsassa-pss"); - var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed(interactionFlowUsed); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'interactionTypeUsed' is empty", exception.getMessage()); - } - - private static SessionResult toSessionResult(String documentNumber) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber(documentNumber); - return sessionResult; - } - - private static SessionSignature toSessionSignature(String signatureAlgorithm) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue("signatureValue"); - sessionSignature.setServerRandom("U2VydmVyUmFuZG9tTW9yZVRoYW4yNENoYXJhY3RlcnM="); - sessionSignature.setUserChallenge("dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdWU"); - - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setFlowType("QR"); - - var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); - signatureAlgorithmParameters.setSaltLength(64); - signatureAlgorithmParameters.setTrailerField("0xbc"); - - var maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm("id-mgf1"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); - return sessionSignature; - } - - private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(AUTH_CERT); - sessionCertificate.setCertificateLevel(QUALIFIED); - return sessionCertificate; - } - - private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setSignatureProtocol("ACSP_V2"); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - sessionStatus.setDeviceIpAddress("0.0.0.0"); - return sessionStatus; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class AuthenticationResponseMapperImplTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + + private AuthenticationResponseMapper authenticationResponseMapper; + + @BeforeEach + void setUp() { + authenticationResponseMapper = new AuthenticationResponseMapperImpl(); + } + + @Test + void from() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @ParameterizedTest + @EnumSource(FlowType.class) + void from_authenticationWithDifferentFlowTypes_ok(FlowType flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.setFlowType(flowType.getDescription()); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void from_authenticationWithDifferentHashAlgorithms_ok(HashAlgorithm hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + sessionSignature.getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm(hashAlgorithm.getAlgorithmName()); + sessionSignature.getSignatureAlgorithmParameters().setSaltLength(hashAlgorithm.getOctetLength()); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "QUALIFIED"); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature, sessionCertificate); + + AuthenticationResponse authenticationResponse = authenticationResponseMapper.from(sessionStatus); + + assertEquals("OK", authenticationResponse.getEndResult()); + assertEquals("signatureValue", authenticationResponse.getSignatureValueInBase64()); + assertEquals(CertificateUtil.toX509Certificate(AUTH_CERT), authenticationResponse.getCertificate()); + assertEquals(AuthenticationCertificateLevel.QUALIFIED, authenticationResponse.getCertificateLevel()); + assertEquals("PNOEE-12345678901-MOCK-Q", authenticationResponse.getDocumentNumber()); + assertEquals("displayTextAndPIN", authenticationResponse.getInteractionTypeUsed()); + assertEquals("0.0.0.0", authenticationResponse.getDeviceIpAddress()); + assertEquals(hashAlgorithm, authenticationResponse.getRsaSsaPssSignatureParameters().getDigestHashAlgorithm()); + assertEquals(hashAlgorithm.getOctetLength(), authenticationResponse.getRsaSsaPssSignatureParameters().getSaltLength()); + } + + @Test + void from_sessionStatusNull_throwException() { + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(null)); + assertEquals("Parameter 'sessionsStatus' is not provided", exception.getMessage()); + } + + @Nested + class ValidateResult { + + @Test + void from_sessionResultIsNotPresent_throwException() { + var sessionStatus = new SessionStatus(); + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result' is empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsNotPresent_throwException(String endResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result.endResult' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void from_endResultIsError_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void from_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> authenticationResponseMapper.from(sessionStatus)); + } + + @ParameterizedTest + @NullAndEmptySource + void from_documentNumberIsEmpty_throwException(String documentNumber) { + var sessionResult = toSessionResult(documentNumber); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'result.documentNumber' is empty", exception.getMessage()); + } + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureProtocolIsNotProvided_throwException(String signatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(signatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signatureProtocol' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"INVALID", "RAW_DIGEST_SIGNATURE"}) + void from_invalidSignatureProtocolIsProvided_throwException(String invalidSignatureProtocol) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol(invalidSignatureProtocol); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signatureProtocol' has unsupported value", exception.getMessage()); + } + + @Nested + class ValidateSignature { + + @Test + void from_signatureIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureValueIsNotProvided_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.value' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|invalidSignatureValue|", "#1234567890"}) + void from_signatureValueDoesNotMatchThePattern_throwException(String signatureValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.value' does not have Base64-encoded value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_serverRandomIsNotProvided_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' is empty", exception.getMessage()); + } + + @Test + void from_serverRandomLengthIsLessThanAllowed_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(23)); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' value length is less than required", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\|YXRsZWFzdDI0Y2hhcmFjdGVycw|", "#YXRsZWFzdDI0Y2hhcmFjdGVycw"}) + void from_serverRandomValueDoesNotMatchThePattern_throwException(String serverRandom) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom(serverRandom); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.serverRandom' does not have Base64-encoded value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_userChallengeIsEmpty_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.userChallenge' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"\\#dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsd", "dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdW="}) + void from_providedUserChallengeDoesNotMatchThePattern_throwException(String userChallenge) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge(userChallenge); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.userChallenge' value does not match required pattern", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_flowTypeNotProvided_throwException(String flowType) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType(flowType); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.flowType' is empty", exception.getMessage()); + } + + @Test + void from_flowTypeNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("NOT_SUPPORTED_FLOW_TYPE"); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.flowType' has unsupported value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_signatureAlgorithmIsNotProvided_throwException(String signatureAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithm); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' is empty", exception.getMessage()); + } + + @Test + void from_signatureAlgorithmIsNotSupported_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("InvalidAlgorithm"); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithm' has unsupported value", exception.getMessage()); + } + + @Nested + class ValidateSignatureAlgorithmParameters { + + @Test + void from_signatureAlgorithmParametersAreMissing_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(null); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmIsMissing_throwException(String hashAlgorithm) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm(invalidHashAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_masGenAlgorithmIsMissing_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_algorithmIsEmptyInMaskGenAlgorithm_throwException(String algorithm) { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(algorithm); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", exception.getMessage()); + } + + @Test + void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("invalid"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_parametersInMaskGenAlgorithmAreMissing_throwException() { + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(null); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_hashAlgorithmInMaskGenAlgorithmParametersIsEmpty_throwException(String hashAlgorithm) { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "asdhfasdf"}) + void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String hashAlgorithm) { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm(hashAlgorithm); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA-256"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); + } + + @Test + void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_throwException() { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA-512"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(maskGenAlgorithmParameters); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); + } + + @Test + void from_saltLengthIsMissing_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(null); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' is empty", exception.getMessage()); + } + + @Test + void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(20); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_trailerFieldIsEmpty_throwException(String trailerField) { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField(trailerField); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' is empty", exception.getMessage()); + } + + @Test + void from_trailerFieldValueIsInvalid_throwException() { + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("invalid"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + maskGenAlgorithm.setParameters(toMaskGenAlgorithmParameters()); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature(signatureAlgorithmParameters); + var sessionStatus = toSessionStatus(sessionResult, sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); + } + + private static SessionSignature toSessionSignature(SessionSignatureAlgorithmParameters signatureAlgorithmParameters) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("a".repeat(24)); + sessionSignature.setUserChallenge("a".repeat(43)); + sessionSignature.setFlowType("QR"); + sessionSignature.setSignatureAlgorithm("rsassa-pss"); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + return sessionStatus; + } + + private static SessionMaskGenAlgorithmParameters toMaskGenAlgorithmParameters() { + var maskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + maskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + return maskGenAlgorithmParameters; + } + } + + @Nested + class ValidateCertificate { + + @Test + void from_sessionCertificateIsNotProvided_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert' is missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.value' is empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_certificateLevelIsNotProvided_throwException(String certificateLevel) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.certificateLevel' is empty", exception.getMessage()); + } + + @Test + void from_certificateIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("invalidCertificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(SmartIdClientException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertTrue(exception.getMessage().startsWith("Failed to parse X509 certificate from")); + } + + @Test + void from_certificateLevelIsInvalid_throwException() { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate(CertificateUtil.getEncodedCertificateData(AUTH_CERT), "invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'cert.certificateLevel' has unsupported value", exception.getMessage()); + } + } + + @ParameterizedTest + @NullAndEmptySource + void from_interactionTypeUsedNotProvided_throwException(String interactionFlowUsed) { + var sessionResult = toSessionResult("PNOEE-12345678901-MOCK-Q"); + var sessionSignature = toSessionSignature("rsassa-pss"); + var sessionCertificate = toSessionCertificate("certificateValue", "QUALIFIED"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed(interactionFlowUsed); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + assertEquals("Authentication session status field 'interactionTypeUsed' is empty", exception.getMessage()); + } + + private static SessionResult toSessionResult(String documentNumber) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber(documentNumber); + return sessionResult; + } + + private static SessionSignature toSessionSignature(String signatureAlgorithm) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue("signatureValue"); + sessionSignature.setServerRandom("U2VydmVyUmFuZG9tTW9yZVRoYW4yNENoYXJhY3RlcnM="); + sessionSignature.setUserChallenge("dXNlcmlzYmVpbmdjaGFsbGVuZ2VkYnl0aGlzdmFsdWU"); + + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setFlowType("QR"); + + var signatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + signatureAlgorithmParameters.setHashAlgorithm("SHA3-512"); + signatureAlgorithmParameters.setSaltLength(64); + signatureAlgorithmParameters.setTrailerField("0xbc"); + + var maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm("id-mgf1"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm("SHA3-512"); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + signatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(signatureAlgorithmParameters); + return sessionSignature; + } + + private static SessionCertificate toSessionCertificate(String AUTH_CERT, String QUALIFIED) { + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(AUTH_CERT); + sessionCertificate.setCertificateLevel(QUALIFIED); + return sessionCertificate; + } + + private static SessionStatus toSessionStatus(SessionResult sessionResult, SessionSignature sessionSignature, SessionCertificate sessionCertificate) { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setSignatureProtocol("ACSP_V2"); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + sessionStatus.setDeviceIpAddress("0.0.0.0"); + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java index f6cf0af7..dcc14ced 100644 --- a/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/CapabilitiesArgumentProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Set; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -public class CapabilitiesArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")), - Arguments.of(new String[]{"capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), - Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{"", "capability1"}, Set.of("capability1")), - Arguments.of(new String[]{" ", "capability1"}, Set.of("capability1")) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class CapabilitiesArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(new String[]{"capability1", "capability2"}, Set.of("capability1", "capability2")), + Arguments.of(new String[]{"capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"capability1", null}, Set.of("capability1")), + Arguments.of(new String[]{null, "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{"", "capability1"}, Set.of("capability1")), + Arguments.of(new String[]{" ", "capability1"}, Set.of("capability1")) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java index 158fec61..3634acfa 100644 --- a/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/CertificateByDocumentNumberRequestBuilderTest.java @@ -1,290 +1,290 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateInfo; -import ee.sk.smartid.rest.dao.CertificateResponse; - -class CertificateByDocumentNumberRequestBuilderTest { - - private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - private static final String RP_UUID = "00000000-0000-0000-0000-000000000000"; - private static final String RP_NAME = "DEMO"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void getCertificateByDocumentNumber_ok() { - CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); - - CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .getCertificateByDocumentNumber(); - - assertNotNull(result); - assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); - assertNotNull(result.certificate()); - - String subject = result.certificate().getSubjectX500Principal().getName(); - assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); - verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); - - CertificateByDocumentNumberRequest sentRequest = captor.getValue(); - assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); - assertEquals(RP_NAME, sentRequest.relyingPartyName()); - assertEquals("QUALIFIED", sentRequest.certificateLevel()); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelSetToNull_ok() { - CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); - - CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withCertificateLevel(null) - .getCertificateByDocumentNumber(); - - assertNotNull(result); - assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); - assertNotNull(result.certificate()); - - String subject = result.certificate().getSubjectX500Principal().getName(); - assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); - verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); - - CertificateByDocumentNumberRequest sentRequest = captor.getValue(); - assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); - assertEquals(RP_NAME, sentRequest.relyingPartyName()); - assertNull(sentRequest.certificateLevel()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_documentNumberMissing_throwException(String documentNumber) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME) - .withDocumentNumber(documentNumber); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_relyingPartyUUIDMissing_throwException(String uuid) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyName(RP_NAME) - .withRelyingPartyUUID(uuid); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void getCertificateByDocumentNumber_relyingPartyNameMissing_throwException(String relyingPartyName) { - var builder = new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(relyingPartyName); - - var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @Test - void getCertificateByDocumentNumber_responseIsNull_throwException() { - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response is not provided", ex.getMessage()); - } - - @Nested - class ValidateState { - - @Test - void getCertificateByDocumentNumber_responseStateMissing_throwException() { - var certificateResponse = new CertificateResponse(null, null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseStateValueIsInvalid_throwException() { - var certificateResponse = new CertificateResponse("invalid", null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseStateIsDocumentUnusable_throwException() { - var certificateResponse = new CertificateResponse(CertificateState.DOCUMENT_UNUSABLE.name(), null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); - } - } - - @Test - void getCertificateByDocumentNumber_certFieldMissing_throwException() { - var certificateResponse = new CertificateResponse(CertificateState.OK.name(), null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert' is missing", ex.getMessage()); - } - - @Nested - class ValidateCertificateLevel { - - @Test - void getCertificateByDocumentNumber_responseCertificateLevelMissing_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, null); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.certificateLevel' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_responseCertificateHasInvalidValue_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, "invalid"); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.certificateLevel' has unsupported value", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelLowerThanRequested_throwException() { - CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate has lower level than requested", ex.getMessage()); - } - } - - @Test - void getCertificateByDocumentNumber_certValueMissing_throwException() { - CertificateResponse response = toCertificateResponse(null, CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); - - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.value' is missing", ex.getMessage()); - } - - @Test - void getCertificateByDocumentNumber_certValueInvalidBase64_throwException() { - CertificateResponse certificateResponse = toCertificateResponse("NOT@BASE64!", CertificateLevel.QUALIFIED.name()); - when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); - var builder = createValidRequestParameters(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'cert.value' does not have Base64-encoded value", ex.getMessage()); - } - } - - private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() { - return new CertificateByDocumentNumberRequestBuilder(connector) - .withDocumentNumber(DOCUMENT_NUMBER) - .withRelyingPartyUUID(RP_UUID) - .withRelyingPartyName(RP_NAME); - } - - private CertificateResponse toCertificateResponse(String certValue, String level) { - var certificate = new CertificateInfo(certValue, level); - return new CertificateResponse(CertificateState.OK.name(), certificate); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateInfo; +import ee.sk.smartid.rest.dao.CertificateResponse; + +class CertificateByDocumentNumberRequestBuilderTest { + + private static final String CERTIFICATE_BASE64 = "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ=="; + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String RP_UUID = "00000000-0000-0000-0000-000000000000"; + private static final String RP_NAME = "DEMO"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void getCertificateByDocumentNumber_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertEquals("QUALIFIED", sentRequest.certificateLevel()); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelSetToNull_ok() { + CertificateResponse mockResponse = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(mockResponse); + + CertificateByDocumentNumberResult result = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withCertificateLevel(null) + .getCertificateByDocumentNumber(); + + assertNotNull(result); + assertEquals(CertificateLevel.QUALIFIED, result.certificateLevel()); + assertNotNull(result.certificate()); + + String subject = result.certificate().getSubjectX500Principal().getName(); + assertTrue(subject.contains("TESTNUMBER") || subject.contains("DEMO"), subject); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CertificateByDocumentNumberRequest.class); + verify(connector).getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), captor.capture()); + + CertificateByDocumentNumberRequest sentRequest = captor.getValue(); + assertEquals(RP_UUID, sentRequest.relyingPartyUUID()); + assertEquals(RP_NAME, sentRequest.relyingPartyName()); + assertNull(sentRequest.certificateLevel()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_documentNumberMissing_throwException(String documentNumber) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME) + .withDocumentNumber(documentNumber); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_relyingPartyUUIDMissing_throwException(String uuid) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyName(RP_NAME) + .withRelyingPartyUUID(uuid); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void getCertificateByDocumentNumber_relyingPartyNameMissing_throwException(String relyingPartyName) { + var builder = new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(relyingPartyName); + + var ex = assertThrows(SmartIdClientException.class, builder::getCertificateByDocumentNumber); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @Test + void getCertificateByDocumentNumber_responseIsNull_throwException() { + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(null); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response is not provided", ex.getMessage()); + } + + @Nested + class ValidateState { + + @Test + void getCertificateByDocumentNumber_responseStateMissing_throwException() { + var certificateResponse = new CertificateResponse(null, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseStateValueIsInvalid_throwException() { + var certificateResponse = new CertificateResponse("invalid", null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseStateIsDocumentUnusable_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.DOCUMENT_UNUSABLE.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + assertThrows(DocumentUnusableException.class, builder::getCertificateByDocumentNumber); + } + } + + @Test + void getCertificateByDocumentNumber_certFieldMissing_throwException() { + var certificateResponse = new CertificateResponse(CertificateState.OK.name(), null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert' is missing", ex.getMessage()); + } + + @Nested + class ValidateCertificateLevel { + + @Test + void getCertificateByDocumentNumber_responseCertificateLevelMissing_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, null); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_responseCertificateHasInvalidValue_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, "invalid"); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelLowerThanRequested_throwException() { + CertificateResponse response = toCertificateResponse(CERTIFICATE_BASE64, CertificateLevel.ADVANCED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate has lower level than requested", ex.getMessage()); + } + } + + @Test + void getCertificateByDocumentNumber_certValueMissing_throwException() { + CertificateResponse response = toCertificateResponse(null, CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(response); + + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.value' is missing", ex.getMessage()); + } + + @Test + void getCertificateByDocumentNumber_certValueInvalidBase64_throwException() { + CertificateResponse certificateResponse = toCertificateResponse("NOT@BASE64!", CertificateLevel.QUALIFIED.name()); + when(connector.getCertificateByDocumentNumber(eq(DOCUMENT_NUMBER), any(CertificateByDocumentNumberRequest.class))).thenReturn(certificateResponse); + var builder = createValidRequestParameters(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'cert.value' does not have Base64-encoded value", ex.getMessage()); + } + } + + private CertificateByDocumentNumberRequestBuilder createValidRequestParameters() { + return new CertificateByDocumentNumberRequestBuilder(connector) + .withDocumentNumber(DOCUMENT_NUMBER) + .withRelyingPartyUUID(RP_UUID) + .withRelyingPartyName(RP_NAME); + } + + private CertificateResponse toCertificateResponse(String certValue, String level) { + var certificate = new CertificateInfo(certValue, level); + return new CertificateResponse(CertificateState.OK.name(), certificate); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java index b3de3d7b..9a1b1d21 100644 --- a/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/CertificateChoiceResponseValidatorTest.java @@ -1,290 +1,290 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionStatus; - -public class CertificateChoiceResponseValidatorTest { - - private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - - CertificateChoiceResponseValidator certificateChoiceResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - } - - @Test - void validate() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void validate_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void validate_returnedCertificateHigherThanRequested_ok() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); - assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void validate_nqCertificate() { - var sessionStatus = toSessionStatus(NQ_SIGNING_CERTIFICATE, "ADVANCED"); - - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - - assertEquals("OK", response.getEndResult()); - assertEquals(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE), response.getCertificate()); - assertEquals(CertificateLevel.ADVANCED, response.getCertificateLevel()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_requestCertificateLevelNotProvided_throwException() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); - - var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus, null)); - assertEquals("Parameter 'requestedCertificateLevel' is not provided", ex.getMessage()); - } - } - - @Nested - class ValidateEndResult { - - @Test - void validate_sessionResultIsNotProvided_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(new SessionStatus())); - assertEquals("Certificate choice session status field 'result' is missing", ex.getMessage()); - } - - @Test - void validate_sessionEndResultIsNotProvided_throwException() { - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(new SessionResult()); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'result.endResult' is empty", ex.getMessage()); - } - - @Test - void validate_sessionDocumentNumberIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'result.documentNumber' is empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void validate_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - } - } - - @Nested - class ValidateCertificate { - - @Test - void validate_sessionCertificateIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(certificateValue); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.value' has empty value", ex.getMessage()); - } - - @Test - void validate_sessionCertificateLevelIsNotProvided_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("INVALID"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.certificateLevel' has empty value", ex.getMessage()); - } - - @Test - void validate_sessionCertificateLevelIsNotSupported_throwException() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue("INVALID"); - sessionCertificate.setCertificateLevel("invalid"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status field 'cert.certificateLevel' has unsupported value", ex.getMessage()); - } - - @Test - void validate_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { - var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "ADVANCED"); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate choice session status response certificate level is lower than requested", ex.getMessage()); - } - - @Test - void validate_expiredCertificateWasReturned() { - var sessionStatus = toSessionStatus(EXPIRED_CERT, "QUALIFIED"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); - assertEquals("Certificate is invalid", ex.getMessage()); - } - } - - private static SessionStatus toSessionStatus(String certificateChoiceCert, String certificateLevel) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(certificateChoiceCert)); - sessionCertificate.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - return sessionStatus; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionStatus; + +public class CertificateChoiceResponseValidatorTest { + + private static final String CERTIFICATE_CHOICE_CERT = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + + CertificateChoiceResponseValidator certificateChoiceResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + } + + @Test + void validate() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested_ok(CertificateLevel requestedCertificateLevel) { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_returnedCertificateHigherThanRequested_ok() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", response.getDocumentNumber()); + assertEquals(CertificateUtil.toX509Certificate(CERTIFICATE_CHOICE_CERT), response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void validate_nqCertificate() { + var sessionStatus = toSessionStatus(NQ_SIGNING_CERTIFICATE, "ADVANCED"); + + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE), response.getCertificate()); + assertEquals(CertificateLevel.ADVANCED, response.getCertificateLevel()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_requestCertificateLevelNotProvided_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "QUALIFIED"); + + var ex = assertThrows(SmartIdClientException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus, null)); + assertEquals("Parameter 'requestedCertificateLevel' is not provided", ex.getMessage()); + } + } + + @Nested + class ValidateEndResult { + + @Test + void validate_sessionResultIsNotProvided_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(new SessionStatus())); + assertEquals("Certificate choice session status field 'result' is missing", ex.getMessage()); + } + + @Test + void validate_sessionEndResultIsNotProvided_throwException() { + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(new SessionResult()); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.endResult' is empty", ex.getMessage()); + } + + @Test + void validate_sessionDocumentNumberIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'result.documentNumber' is empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void validate_sessionEndResultIsNotOk_throwException(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + } + } + + @Nested + class ValidateCertificate { + + @Test + void validate_sessionCertificateIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_sessionCertificateValueIsNotProvided_throwException(String certificateValue) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(certificateValue); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.value' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotProvided_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has empty value", ex.getMessage()); + } + + @Test + void validate_sessionCertificateLevelIsNotSupported_throwException() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue("INVALID"); + sessionCertificate.setCertificateLevel("invalid"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status field 'cert.certificateLevel' has unsupported value", ex.getMessage()); + } + + @Test + void validate_sessionRequestCertificateLevelIsLowerThanRequested_throwException() { + var sessionStatus = toSessionStatus(CERTIFICATE_CHOICE_CERT, "ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate choice session status response certificate level is lower than requested", ex.getMessage()); + } + + @Test + void validate_expiredCertificateWasReturned() { + var sessionStatus = toSessionStatus(EXPIRED_CERT, "QUALIFIED"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateChoiceResponseValidator.validate(sessionStatus)); + assertEquals("Certificate is invalid", ex.getMessage()); + } + } + + private static SessionStatus toSessionStatus(String certificateChoiceCert, String certificateLevel) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-40504040001-MOCK-Q"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(certificateChoiceCert)); + sessionCertificate.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + return sessionStatus; + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateParserTest.java b/src/test/java/ee/sk/smartid/CertificateParserTest.java index ae230487..66947565 100644 --- a/src/test/java/ee/sk/smartid/CertificateParserTest.java +++ b/src/test/java/ee/sk/smartid/CertificateParserTest.java @@ -1,41 +1,41 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -public class CertificateParserTest { - - @Test - public void testBothCertificateLevelsQualified() { - assertThrows(SmartIdClientException.class, () -> CertificateParser.parseX509Certificate("invalid")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class CertificateParserTest { + + @Test + public void testBothCertificateLevelsQualified() { + assertThrows(SmartIdClientException.class, () -> CertificateParser.parseX509Certificate("invalid")); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateUtil.java b/src/test/java/ee/sk/smartid/CertificateUtil.java index 4fa9a2e3..dc96b6c7 100644 --- a/src/test/java/ee/sk/smartid/CertificateUtil.java +++ b/src/test/java/ee/sk/smartid/CertificateUtil.java @@ -1,72 +1,72 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -public final class CertificateUtil { - - private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; - private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; - - private CertificateUtil() { - } - - public static X509Certificate toX509Certificate(byte[] certificateBytes) throws CertificateException { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); - } - - public static X509Certificate toX509Certificate(String certificate) { - try { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); - } catch (CertificateException e) { - throw new RuntimeException(e); - } - } - - public static X509Certificate toX509CertificateFromEncodedString(String base64Certificate) throws CertificateException { - byte[] certificateBytes = getX509CertificateBytes(base64Certificate); - return toX509Certificate(certificateBytes); - } - - public static String getEncodedCertificateData(String certificate) { - return certificate.replace(BEGIN_CERTIFICATE, "") - .replace(END_CERTIFICATE, "") - .replace("\n", ""); - } - - private static byte[] getX509CertificateBytes(String encodedData) { - String certificate = BEGIN_CERTIFICATE + "\n" + encodedData + "\n" + END_CERTIFICATE; - return certificate.getBytes(StandardCharsets.UTF_8); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public final class CertificateUtil { + + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; + + private CertificateUtil() { + } + + public static X509Certificate toX509Certificate(byte[] certificateBytes) throws CertificateException { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificateBytes)); + } + + public static X509Certificate toX509Certificate(String certificate) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certificate.getBytes(StandardCharsets.UTF_8))); + } catch (CertificateException e) { + throw new RuntimeException(e); + } + } + + public static X509Certificate toX509CertificateFromEncodedString(String base64Certificate) throws CertificateException { + byte[] certificateBytes = getX509CertificateBytes(base64Certificate); + return toX509Certificate(certificateBytes); + } + + public static String getEncodedCertificateData(String certificate) { + return certificate.replace(BEGIN_CERTIFICATE, "") + .replace(END_CERTIFICATE, "") + .replace("\n", ""); + } + + private static byte[] getX509CertificateBytes(String encodedData) { + String certificate = BEGIN_CERTIFICATE + "\n" + encodedData + "\n" + END_CERTIFICATE; + return certificate.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java index f8fe8f0e..a3e29fa7 100644 --- a/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/CertificateValidatorImplTest.java @@ -1,77 +1,77 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class CertificateValidatorImplTest { - - private static final String TRUSTED_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - private static final String NOT_TRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); - private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); - - private CertificateValidatorImpl certificateValidator; - - @BeforeEach - void setUp() { - certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build()); - } - - @Test - void validate_ok() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); - - assertDoesNotThrow(() -> certificateValidator.validate(certificate)); - } - - @Test - void validate_expired() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); - assertEquals("Certificate is invalid", exception.getMessage()); - } - - @Test - void validate_notTrusted() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); - assertEquals("Certificate chain validation failed", exception.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class CertificateValidatorImplTest { + + private static final String TRUSTED_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String NOT_TRUSTED_CERT = FileUtil.readFileToString("test-certs/other-auth-cert.pem.crt"); + private static final String EXPIRED_CERT = FileUtil.readFileToString("test-certs/expired-cert.pem.crt"); + + private CertificateValidatorImpl certificateValidator; + + @BeforeEach + void setUp() { + certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().withOcspEnabled(false).build()); + } + + @Test + void validate_ok() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + assertDoesNotThrow(() -> certificateValidator.validate(certificate)); + } + + @Test + void validate_expired() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(EXPIRED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); + assertEquals("Certificate is invalid", exception.getMessage()); + } + + @Test + void validate_notTrusted() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509Certificate(NOT_TRUSTED_CERT.getBytes(StandardCharsets.UTF_8)); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> certificateValidator.validate(certificate)); + assertEquals("Certificate chain validation failed", exception.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java index d2dd6f7c..8b8590a2 100644 --- a/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java +++ b/src/test/java/ee/sk/smartid/ClientRequestHeaderFilter.java @@ -1,51 +1,51 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.Map; - -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; -import jakarta.ws.rs.core.MultivaluedMap; - -public class ClientRequestHeaderFilter implements ClientRequestFilter { - - private final Map headersToAdd; - - public ClientRequestHeaderFilter(Map headersToAdd) { - this.headersToAdd = headersToAdd; - } - - @Override - public void filter(ClientRequestContext requestContext) { - MultivaluedMap headers = requestContext.getHeaders(); - for (Map.Entry entry : headersToAdd.entrySet()) { - headers.putSingle(entry.getKey(), entry.getValue()); - } - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.Map; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.MultivaluedMap; + +public class ClientRequestHeaderFilter implements ClientRequestFilter { + + private final Map headersToAdd; + + public ClientRequestHeaderFilter(Map headersToAdd) { + this.headersToAdd = headersToAdd; + } + + @Override + public void filter(ClientRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + for (Map.Entry entry : headersToAdd.entrySet()) { + headers.putSingle(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java index b52bef7f..485f5d81 100644 --- a/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DefaultTrustedCAStoreBuilderTest.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -class DefaultTrustedCAStoreBuilderTest { - - private static final String TRUST_ANCHOR_CERT = FileUtil.readFileToString("test-certs/TEST_SK_ROOT_G1_2021E.pem.crt"); - private static final String INTERMEDIATE_CA_CERT = FileUtil.readFileToString("trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt"); - private static final String OCSP_CERT = FileUtil.readFileToString("test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer"); - - @Test - void buildDefaultTrustedCACertStore_ocspValidationDisabled() { - X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); - TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); - new DefaultTrustedCAStoreBuilder() - .withTrustAnchors(Set.of(trustAnchor)) - .withIntermediateCACertificate(List.of(intermediateCACertificate)) - .withOcspEnabled(false) - .build(); - } - - @Disabled("Fails with OCSP response validation error, needs investigation") - @Test - void buildDefaultTrustedCACertStore_ocspValidationEnabled() { - X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); - TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); - X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); - new DefaultTrustedCAStoreBuilder() - .withTrustAnchors(Set.of(trustAnchor)) - .withIntermediateCACertificate(List.of(intermediateCACertificate)) - .withOcspEnabled(true) - .withOCSPValidationCert(CertificateUtil.toX509Certificate(OCSP_CERT)) - .build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class DefaultTrustedCAStoreBuilderTest { + + private static final String TRUST_ANCHOR_CERT = FileUtil.readFileToString("test-certs/TEST_SK_ROOT_G1_2021E.pem.crt"); + private static final String INTERMEDIATE_CA_CERT = FileUtil.readFileToString("trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt"); + private static final String OCSP_CERT = FileUtil.readFileToString("test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer"); + + @Test + void buildDefaultTrustedCACertStore_ocspValidationDisabled() { + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(false) + .build(); + } + + @Disabled("Fails with OCSP response validation error, needs investigation") + @Test + void buildDefaultTrustedCACertStore_ocspValidationEnabled() { + X509Certificate trustAnchorCertificate = CertificateUtil.toX509Certificate(TRUST_ANCHOR_CERT); + TrustAnchor trustAnchor = new TrustAnchor(trustAnchorCertificate, null); + X509Certificate intermediateCACertificate = CertificateUtil.toX509Certificate(INTERMEDIATE_CA_CERT); + new DefaultTrustedCAStoreBuilder() + .withTrustAnchors(Set.of(trustAnchor)) + .withIntermediateCACertificate(List.of(intermediateCACertificate)) + .withOcspEnabled(true) + .withOCSPValidationCert(CertificateUtil.toX509Certificate(OCSP_CERT)) + .build(); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java index d7afcf17..1f1ba798 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationResponseValidatorTest.java @@ -1,267 +1,267 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; -import java.util.Base64; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; - -class DeviceLinkAuthenticationResponseValidatorTest { - - private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - - private DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - } - - @Disabled("Will be fixed when testing with DEMO accounts will be possible") - @Test - void validate_ok() { - String rpChallenge = ""; - SessionStatus sessionStatus = new SessionStatus(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } - - @Disabled - @Test - void validate_qrCodeWasUsedDoNotIncludeInitialCallbackUrlInSignatureValidation_ok() { - // TODO - 26.09.25: implement with demo accounts - } - - @Disabled - @Test - void validate_initialCallbackUrlWasUsed_ok() { - // TODO - 26.09.25: implement with demo accounts - } - - @Disabled("Will be fixed when testing with DEMO accounts will be possible") - @Test - void validate_certificateLevelHigherThanRequested_ok() { - SessionStatus sessionStatus = new SessionStatus(); - SessionCertificate cert = new SessionCertificate(); - cert.setCertificateLevel("QUALIFIED"); - sessionStatus.setCert(cert); - - var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, null, "smart-id-demo", null)); - assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, schemaName, null)); - assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); - } - } - - @Test - void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); - } - - @Nested - class ValidateUserChallenge { - - @ParameterizedTest - @NullAndEmptySource - void validate_sameDeviceFlowButUserChallengeVerifierNotProvided_throwException(String userChallengeVerifier) { - var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", "", "Cjy8feLy_DB1GNF6lLpXf0VbzCMfTaLHzYOOpdXevSc", FlowType.WEB2APP); - - var ex = assertThrows(SmartIdClientException.class, - () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), userChallengeVerifier, "smart-id-demo", null)); - - assertEquals("Parameter 'userChallengeVerifier' must be provided for 'flowType' - WEB2APP", ex.getMessage()); - } - } - - @Nested - class ValidateSessionStatusCertificate { - - @Test - void validate_certificateLevelLowerThanRequested_throwException() { - var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", ""); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var sessionStatus = toSessionStatus(SIGN_CERT, "QUALIFIED", ""); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - } - - @Nested - class ValidateAuthenticationSignature { - - @Test - void validate_invalidSignature_throwException() { - var sessionStatus = toSessionStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); - - assertEquals("Signature value validation failed", ex.getMessage()); - } - } - - private static SessionStatus toSessionStatus(String certificateValue, - String certificateLevel, - String signatureValue) { - return toSessionStatus(certificateValue, certificateLevel, signatureValue, "TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4", FlowType.QR); - } - - private static SessionStatus toSessionStatus(String certificateValue, - String certificateLevel, - String signatureValue, - String userChallengeVerifier, - FlowType flowType) { - var result = new SessionResult(); - result.setEndResult("OK"); - result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - - SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - - var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); - sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); - sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var signature = new SessionSignature(); - signature.setServerRandom(toBase64("a".repeat(43))); - signature.setUserChallenge(userChallengeVerifier); - signature.setValue(toBase64("signatureValue")); - signature.setFlowType(flowType.getDescription()); - signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); - - var cert = new SessionCertificate(); - cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); - cert.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(result); - sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); - sessionStatus.setSignature(signature); - sessionStatus.setCert(cert); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - return sessionStatus; - } - - private static DeviceLinkAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { - return new DeviceLinkAuthenticationSessionRequest( - "00000000-0000-0000-0000-000000000001", - "DEMO", - certificateLevel, - SignatureProtocol.ACSP_V2, - new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - null, - null, - null); - } - - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; + +class DeviceLinkAuthenticationResponseValidatorTest { + + private static final String CA_CERT = FileUtil.readFileToString("test-certs/ca-cert.pem.crt"); + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + + private DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + } + + @Disabled("Will be fixed when testing with DEMO accounts will be possible") + @Test + void validate_ok() { + String rpChallenge = ""; + SessionStatus sessionStatus = new SessionStatus(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = toAuthenticationSessionRequest("QUALIFIED"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Disabled + @Test + void validate_qrCodeWasUsedDoNotIncludeInitialCallbackUrlInSignatureValidation_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled + @Test + void validate_initialCallbackUrlWasUsed_ok() { + // TODO - 26.09.25: implement with demo accounts + } + + @Disabled("Will be fixed when testing with DEMO accounts will be possible") + @Test + void validate_certificateLevelHigherThanRequested_ok() { + SessionStatus sessionStatus = new SessionStatus(); + SessionCertificate cert = new SessionCertificate(); + cert.setCertificateLevel("QUALIFIED"); + sessionStatus.setCert(cert); + + var authenticationSessionRequest = toAuthenticationSessionRequest("ADVANCED"); + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + assertEquals(Optional.of(LocalDate.of(1905, 4, 4)), authenticationIdentity.getDateOfBirth()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), null, null, "smart-id-demo", null)); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, schemaName, null)); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); + } + } + + @Test + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> deviceLinkAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); + } + + @Nested + class ValidateUserChallenge { + + @ParameterizedTest + @NullAndEmptySource + void validate_sameDeviceFlowButUserChallengeVerifierNotProvided_throwException(String userChallengeVerifier) { + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", "", "Cjy8feLy_DB1GNF6lLpXf0VbzCMfTaLHzYOOpdXevSc", FlowType.WEB2APP); + + var ex = assertThrows(SmartIdClientException.class, + () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), userChallengeVerifier, "smart-id-demo", null)); + + assertEquals("Parameter 'userChallengeVerifier' must be provided for 'flowType' - WEB2APP", ex.getMessage()); + } + } + + @Nested + class ValidateSessionStatusCertificate { + + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionStatus(AUTH_CERT, "ADVANCED", ""); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var sessionStatus = toSessionStatus(SIGN_CERT, "QUALIFIED", ""); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + } + + @Nested + class ValidateAuthenticationSignature { + + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> deviceLinkAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), null, "smart-id-demo", null)); + + assertEquals("Signature value validation failed", ex.getMessage()); + } + } + + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue) { + return toSessionStatus(certificateValue, certificateLevel, signatureValue, "TLSjYRH2oYw8tW2bq0it0IUb7WIFkCLgF8NTc7-4Zq4", FlowType.QR); + } + + private static SessionStatus toSessionStatus(String certificateValue, + String certificateLevel, + String signatureValue, + String userChallengeVerifier, + FlowType flowType) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-1234567890-MOCK-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom(toBase64("a".repeat(43))); + signature.setUserChallenge(userChallengeVerifier); + signature.setValue(toBase64("signatureValue")); + signature.setFlowType(flowType.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; + } + + private static DeviceLinkAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new DeviceLinkAuthenticationSessionRequest( + "00000000-0000-0000-0000-000000000001", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2, + new AcspV2SignatureProtocolParameters("rpChallenge", SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + null, + null, + null); + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java index 428bcca5..e5e007a2 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkAuthenticationSessionRequestBuilderTest.java @@ -1,481 +1,481 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import com.fasterxml.jackson.databind.ObjectMapper; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class DeviceLinkAuthenticationSessionRequestBuilderTest { - - private static final String BASE64_PATTERN = "^[A-Za-z0-9+/]+={0,2}$"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Nested - class ValidateRequiredRequestParameters { - - @Test - void initAuthenticationSession_anonymousAuthentication_ok() throws Exception { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void initAuthenticationSession_withDocumentNumber_ok() { - when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withDocumentNumber("PNOEE-48010010101-MOCK-Q")); - - builder.initAuthenticationSession(); - - ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); - verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); - String capturedDocumentNumber = documentNumberCaptor.getValue(); - - assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); - } - - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - builder.initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCertificateLevel(certificateLevel)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - } - - @Test - void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toBaseDeviceLinkRequestBuilder().initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNull(request.requestProperties()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) - .thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) - .initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initAuthenticationSession_capabilities_ok(String capabilities) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - - toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @Test - void initAuthenticationSession_initialCallbackUrlIsValid_ok() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl("https://example.com/callback")); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); - verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); - DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals("https://example.com/callback", request.initialCallbackUrl()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) - void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_interactionsIsEmpty_throwException(List interactions) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsEmpty_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = - toDeviceLinkRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(duplicateInteractions)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl(url)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals(expectedErrorMessage, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { - DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> - b.withDocumentNumber("PNOEE-48010010101-MOCK-Q") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(response); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); - } - } - - @Test - void getAuthenticationSessionRequest_ok() throws Exception { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { - when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); - DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); - assertEquals("Device link authentication session has not been initialized yet", ex.getMessage()); - } - - private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseDeviceLinkRequestBuilder()); - } - - private DeviceLinkAuthenticationSessionRequestBuilder toBaseDeviceLinkRequestBuilder() { - return new DeviceLinkAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPin("Log into internet banking system"))); - } - - private DeviceLinkSessionResponse toDeviceLinkAuthenticationResponse() { - return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", - generateBase64String("sessionToken"), - generateBase64String("sessionSecret"), - URI.create("https://example.com/callback")); - } - - private static String generateBase64String(String text) { - return Base64.toBase64String(text.getBytes()); - } - - private void assertAuthenticationSessionRequest(DeviceLinkAuthenticationSessionRequest request) throws Exception { - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals("QUALIFIED", request.certificateLevel()); - assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertNotNull(request.signatureProtocolParameters().rpChallenge()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); - - Interaction[] parsed = parseInteractionsFromBase64(request.interactions()); - assertTrue(Stream.of(parsed).anyMatch(i -> i.type().equals("displayTextAndPIN"))); - } - - private Interaction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { - byte[] decodedBytes = Base64.decode(base64EncodedJson); - String json = new String(decodedBytes, StandardCharsets.UTF_8); - var mapper = new ObjectMapper(); - return mapper.readValue(json, Interaction[].class); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("https://example.com|test", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), - Arguments.of("ftp://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class DeviceLinkAuthenticationSessionRequestBuilderTest { + + private static final String BASE64_PATTERN = "^[A-Za-z0-9+/]+={0,2}$"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Nested + class ValidateRequiredRequestParameters { + + @Test + void initAuthenticationSession_anonymousAuthentication_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withDocumentNumber("PNOEE-48010010101-MOCK-Q")); + + builder.initAuthenticationSession(); + + ArgumentCaptor documentNumberCaptor = ArgumentCaptor.forClass(String.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), documentNumberCaptor.capture()); + String capturedDocumentNumber = documentNumberCaptor.getValue(); + + assertEquals("PNOEE-48010010101-MOCK-Q", capturedDocumentNumber); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + builder.initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCertificateLevel(certificateLevel)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + } + + @Test + void initAuthenticationSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toBaseDeviceLinkRequestBuilder().initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNull(request.requestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingRequired_ok(boolean ipRequested) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))) + .thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) + .initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initAuthenticationSession_capabilities_ok(String capabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initAuthenticationSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + + toDeviceLinkRequestBuilder(b -> b.withCapabilities(capabilities)).initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @Test + void initAuthenticationSession_initialCallbackUrlIsValid_ok() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl("https://example.com/callback")); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkAuthenticationSessionRequest.class); + verify(connector).initAnonymousDeviceLinkAuthentication(requestCaptor.capture()); + DeviceLinkAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals("https://example.com/callback", request.initialCallbackUrl()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_interactionsIsEmpty_throwException(List interactions) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsEmpty_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = + toDeviceLinkRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initAuthenticationSession_duplicateInteractions_throwException(List duplicateInteractions) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInteractions(duplicateInteractions)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initAuthenticationSession_initialCallbackUrlIsInvalid_throwException(String url, String expectedErrorMessage) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals(expectedErrorMessage, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersIsNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmParametersHashAlgorithmIsNull_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_bothSemanticsIdentifierAndDocumentNumberSet_throwException() { + DeviceLinkAuthenticationSessionRequestBuilder builder = toDeviceLinkRequestBuilder(b -> + b.withDocumentNumber("PNOEE-48010010101-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initAuthenticationSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse(sessionId, null, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionTokenIsNotPresentInTheResponse_throwException(String sessionToken) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", sessionToken, null, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionToken' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionSecretIsNotPresentInTheResponse_throwException(String sessionSecret) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var deviceLinkAuthenticationSessionResponse = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), sessionSecret, null); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(deviceLinkAuthenticationSessionResponse); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'sessionSecret' is missing or empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + var response = new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", generateBase64String("sessionToken"), generateBase64String("sessionSecret"), deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(response); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Device link authentication session initialisation response field 'deviceLinkBase' is missing or empty", exception.getMessage()); + } + } + + @Test + void getAuthenticationSessionRequest_ok() throws Exception { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initAnonymousDeviceLinkAuthentication(any(DeviceLinkAuthenticationSessionRequest.class))).thenReturn(toDeviceLinkAuthenticationResponse()); + DeviceLinkAuthenticationSessionRequestBuilder builder = toBaseDeviceLinkRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Device link authentication session has not been initialized yet", ex.getMessage()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toDeviceLinkRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseDeviceLinkRequestBuilder()); + } + + private DeviceLinkAuthenticationSessionRequestBuilder toBaseDeviceLinkRequestBuilder() { + return new DeviceLinkAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(Collections.singletonList(DeviceLinkInteraction.displayTextAndPin("Log into internet banking system"))); + } + + private DeviceLinkSessionResponse toDeviceLinkAuthenticationResponse() { + return new DeviceLinkSessionResponse("00000000-0000-0000-0000-000000000000", + generateBase64String("sessionToken"), + generateBase64String("sessionSecret"), + URI.create("https://example.com/callback")); + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private void assertAuthenticationSessionRequest(DeviceLinkAuthenticationSessionRequest request) throws Exception { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals("QUALIFIED", request.certificateLevel()); + assertEquals(SignatureProtocol.ACSP_V2, request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertNotNull(request.signatureProtocolParameters().rpChallenge()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + assertTrue(Pattern.matches(BASE64_PATTERN, request.signatureProtocolParameters().rpChallenge())); + + Interaction[] parsed = parseInteractionsFromBase64(request.interactions()); + assertTrue(Stream.of(parsed).anyMatch(i -> i.type().equals("displayTextAndPIN"))); + } + + private Interaction[] parseInteractionsFromBase64(String base64EncodedJson) throws Exception { + byte[] decodedBytes = Base64.decode(base64EncodedJson); + String json = new String(decodedBytes, StandardCharsets.UTF_8); + var mapper = new ObjectMapper(); + return mapper.readValue(json, Interaction[].class); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("https://example.com|test", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars"), + Arguments.of("ftp://example.com", "Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java index 669c4867..cdd8f4ec 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkBuilderTest.java @@ -1,550 +1,550 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.matchesPattern; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; -import java.util.Map; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class DeviceLinkBuilderTest { - - private static final String SESSION_SECRET = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); - private static final String DEMO_SCHEMA_NAME = "smart-id-demo"; - private static final String DEVICE_LINK_BASE = "https://smart-id.com/device-link/"; - private static final String DEVICE_LINK_HOST = "smart-id.com"; - private static final String SESSION_TOKEN = "token123"; - private static final String LANGUAGE = "eng"; - private static final String VERSION_INVALID = "0.9"; - private static final long ELAPSED_SECONDS = 1L; - private static final String CALLBACK_URL = "https://callback.url"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final String BASE64_DIGEST = "dGVzdC1kaWdlc3Q="; - private static final String BROKERED_RP = "QlJP"; - private static final String BASE64_INTERACTIONS = "SW50ZXJhY3Rpb25z"; - private static final String AUTH_CODE_PATTERN = "^[A-Za-z0-9_-]{43}$"; - - @Nested - class CreateUnprotectedUri { - - @ParameterizedTest - @EnumSource - void createUri_validInputs_shouldBuildUri(DeviceLinkType deviceLinkType) { - URI uri = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(deviceLinkType) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(deviceLinkType == DeviceLinkType.QR_CODE ? ELAPSED_SECONDS : null) - .createUnprotectedUri(); - - assertThat(uri.getHost(), equalTo(DEVICE_LINK_HOST)); - } - - @Test - void createUri_invalidVersion_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withVersion(VERSION_INVALID) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Only version 1.0 is allowed", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingDeviceLinkBase_throwsException(String base) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(base) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'deviceLinkBase' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingVersion_throwsException(String version) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withVersion(version) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'version' cannot be empty", ex.getMessage()); - } - - @Test - void createUri_missingDeviceLinkType_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(null) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'deviceLinkType' must be set", ex.getMessage()); - } - - @Test - void createUri_missingSessionType_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'sessionType' must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingSessionToken_throwsException(String token) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(token) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'sessionToken' cannot be empty", ex.getMessage()); - } - - @Test - void createUri_missingElapsedSecondsForQrCode_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .createUnprotectedUri() - ); - assertEquals("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void createUri_missingLang_throwsException(String lang) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(lang) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'lang' must be set", ex.getMessage()); - } - - @Test - void createUri_elapsedSecondsSetForNonQrCode_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .createUnprotectedUri() - ); - assertEquals("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE", ex.getMessage()); - } - } - - @Nested - class BuildDeviceLink { - - @ParameterizedTest - @EnumSource(value = SessionType.class) - void buildDeviceLink(SessionType sessionType) { - DeviceLinkBuilder builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(sessionType) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME); - - if (sessionType != SessionType.CERTIFICATE_CHOICE) { - builder.withDigest(BASE64_DIGEST) - .withInteractions(BASE64_INTERACTIONS); - } - - URI uri = builder.buildDeviceLink(SESSION_SECRET); - - Map params = toQueryParamsMap(uri); - assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); - } - - @Test - void buildDeviceLink_withCustomSchemeName() { - String authCode = toQueryParamsMap( - new DeviceLinkBuilder() - .withSchemeName(DEMO_SCHEMA_NAME) - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(BASE64_INTERACTIONS) - .buildDeviceLink(SESSION_SECRET) - ).get("authCode"); - - assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); - } - - @Test - void buildDeviceLink_sameDeviceFlowWithCallback_ok() { - URI uri = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withInitialCallbackUrl(CALLBACK_URL) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET); - - Map params = toQueryParamsMap(uri); - assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingSchemeName_throwsException(String scheme) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withSchemeName(scheme) - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withLang(LANGUAGE) - .withElapsedSeconds(ELAPSED_SECONDS) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'schemeName' cannot be empty", ex.getMessage()); - } - - @Test - void buildDeviceLink_missingRelyingPartyName_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingDigestForAuthentication_throwsException(String digest) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(digest) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_missingDigestForSignature_throwsException(String digest) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.SIGNATURE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(digest) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @Test - void buildDeviceLink_certificateChoiceAndDigestIsSet_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withDigest(BASE64_DIGEST) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); - } - - @Test - void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { - var exception = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInitialCallbackUrl(CALLBACK_URL) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE", exception.getMessage()); - } - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"APP_2_APP", "WEB_2_APP"}) - void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLinkType deviceLinkType) { - var exception = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(deviceLinkType) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_interactionsMissingForAuthentication_throwsException(String interactions) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(BASE64_DIGEST) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(interactions) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_interactionsMissingForSignature_throwsException(String interactions) { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.SIGNATURE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withDigest(BASE64_DIGEST) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(interactions) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); - } - - @Test - void buildDeviceLink_interactionsSetForCertificateChoice_throwsException() { - var ex = assertThrows(SmartIdClientException.class, () -> - new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withRelyingPartyName(RELYING_PARTY_NAME) - .buildDeviceLink(SESSION_SECRET) - ); - assertEquals("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); - } - - @Test - void buildDeviceLink_invalidBase64Key_shouldThrowException() { - var builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME); - - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); - - assertEquals("Failed to calculate authCode", exception.getMessage()); - assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); - } - - @ParameterizedTest - @NullAndEmptySource - void buildDeviceLink_sessionSecretIsEmpty_throwException(String sessionSecret) { - var builder = new DeviceLinkBuilder() - .withDeviceLinkBase(DEVICE_LINK_BASE) - .withSessionToken(SESSION_TOKEN) - .withSessionType(SessionType.AUTHENTICATION) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withBrokeredRpName(BROKERED_RP) - .withInteractions(BASE64_INTERACTIONS) - .withLang(LANGUAGE) - .withElapsedSeconds(1L) - .withDigest(BASE64_DIGEST) - .withRelyingPartyName(RELYING_PARTY_NAME); - - var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(sessionSecret)); - - assertEquals("Parameter 'sessionSecret' cannot be empty", exception.getMessage()); - } - } - - private static Map toQueryParamsMap(URI uri) { - return Arrays.stream(uri.getQuery().split("&")) - .map(s -> s.split("=")) - .collect(Collectors.toMap(s -> s[0], s -> s[1])); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class DeviceLinkBuilderTest { + + private static final String SESSION_SECRET = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + private static final String DEMO_SCHEMA_NAME = "smart-id-demo"; + private static final String DEVICE_LINK_BASE = "https://smart-id.com/device-link/"; + private static final String DEVICE_LINK_HOST = "smart-id.com"; + private static final String SESSION_TOKEN = "token123"; + private static final String LANGUAGE = "eng"; + private static final String VERSION_INVALID = "0.9"; + private static final long ELAPSED_SECONDS = 1L; + private static final String CALLBACK_URL = "https://callback.url"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final String BASE64_DIGEST = "dGVzdC1kaWdlc3Q="; + private static final String BROKERED_RP = "QlJP"; + private static final String BASE64_INTERACTIONS = "SW50ZXJhY3Rpb25z"; + private static final String AUTH_CODE_PATTERN = "^[A-Za-z0-9_-]{43}$"; + + @Nested + class CreateUnprotectedUri { + + @ParameterizedTest + @EnumSource + void createUri_validInputs_shouldBuildUri(DeviceLinkType deviceLinkType) { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(deviceLinkType) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(deviceLinkType == DeviceLinkType.QR_CODE ? ELAPSED_SECONDS : null) + .createUnprotectedUri(); + + assertThat(uri.getHost(), equalTo(DEVICE_LINK_HOST)); + } + + @Test + void createUri_invalidVersion_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withVersion(VERSION_INVALID) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Only version 1.0 is allowed", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingDeviceLinkBase_throwsException(String base) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(base) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'deviceLinkBase' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingVersion_throwsException(String version) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withVersion(version) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'version' cannot be empty", ex.getMessage()); + } + + @Test + void createUri_missingDeviceLinkType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(null) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'deviceLinkType' must be set", ex.getMessage()); + } + + @Test + void createUri_missingSessionType_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'sessionType' must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingSessionToken_throwsException(String token) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(token) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'sessionToken' cannot be empty", ex.getMessage()); + } + + @Test + void createUri_missingElapsedSecondsForQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .createUnprotectedUri() + ); + assertEquals("Parameter 'elapsedSeconds' must be set when 'deviceLinkType' is QR_CODE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void createUri_missingLang_throwsException(String lang) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(lang) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'lang' must be set", ex.getMessage()); + } + + @Test + void createUri_elapsedSecondsSetForNonQrCode_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .createUnprotectedUri() + ); + assertEquals("Parameter 'elapsedSeconds' should only be used when 'deviceLinkType' is QR_CODE", ex.getMessage()); + } + } + + @Nested + class BuildDeviceLink { + + @ParameterizedTest + @EnumSource(value = SessionType.class) + void buildDeviceLink(SessionType sessionType) { + DeviceLinkBuilder builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(sessionType) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME); + + if (sessionType != SessionType.CERTIFICATE_CHOICE) { + builder.withDigest(BASE64_DIGEST) + .withInteractions(BASE64_INTERACTIONS); + } + + URI uri = builder.buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + + @Test + void buildDeviceLink_withCustomSchemeName() { + String authCode = toQueryParamsMap( + new DeviceLinkBuilder() + .withSchemeName(DEMO_SCHEMA_NAME) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(BASE64_INTERACTIONS) + .buildDeviceLink(SESSION_SECRET) + ).get("authCode"); + + assertThat(authCode, matchesPattern(AUTH_CODE_PATTERN)); + } + + @Test + void buildDeviceLink_sameDeviceFlowWithCallback_ok() { + URI uri = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withInitialCallbackUrl(CALLBACK_URL) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET); + + Map params = toQueryParamsMap(uri); + assertThat(params.get("authCode"), matchesPattern(AUTH_CODE_PATTERN)); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingSchemeName_throwsException(String scheme) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withSchemeName(scheme) + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withLang(LANGUAGE) + .withElapsedSeconds(ELAPSED_SECONDS) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'schemeName' cannot be empty", ex.getMessage()); + } + + @Test + void buildDeviceLink_missingRelyingPartyName_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForAuthentication_throwsException(String digest) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_missingDigestForSignature_throwsException(String digest) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(digest) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @Test + void buildDeviceLink_certificateChoiceAndDigestIsSet_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDigest(BASE64_DIGEST) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'digest' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); + } + + @Test + void buildDeviceLink_qrCodeWithCallback_shouldThrowException() { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInitialCallbackUrl(CALLBACK_URL) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'initialCallbackUrl' must be empty when 'deviceLinkType' is QR_CODE", exception.getMessage()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"APP_2_APP", "WEB_2_APP"}) + void buildDeviceLink_sameDeviceFlowWithoutCallback_shouldThrowException(DeviceLinkType deviceLinkType) { + var exception = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(deviceLinkType) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'initialCallbackUrl' must be provided when 'deviceLinkType' is APP_2_APP or WEB_2_APP", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForAuthentication_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_interactionsMissingForSignature_throwsException(String interactions) { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.SIGNATURE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withDigest(BASE64_DIGEST) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(interactions) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be set when 'sessionType' is AUTHENTICATION or SIGNATURE", ex.getMessage()); + } + + @Test + void buildDeviceLink_interactionsSetForCertificateChoice_throwsException() { + var ex = assertThrows(SmartIdClientException.class, () -> + new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withRelyingPartyName(RELYING_PARTY_NAME) + .buildDeviceLink(SESSION_SECRET) + ); + assertEquals("Parameter 'interactions' must be empty when 'sessionType' is CERTIFICATE_CHOICE", ex.getMessage()); + } + + @Test + void buildDeviceLink_invalidBase64Key_shouldThrowException() { + var builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink("!!!invalidBase64===")); + + assertEquals("Failed to calculate authCode", exception.getMessage()); + assertThat(exception.getCause(), org.hamcrest.Matchers.instanceOf(IllegalArgumentException.class)); + } + + @ParameterizedTest + @NullAndEmptySource + void buildDeviceLink_sessionSecretIsEmpty_throwException(String sessionSecret) { + var builder = new DeviceLinkBuilder() + .withDeviceLinkBase(DEVICE_LINK_BASE) + .withSessionToken(SESSION_TOKEN) + .withSessionType(SessionType.AUTHENTICATION) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withBrokeredRpName(BROKERED_RP) + .withInteractions(BASE64_INTERACTIONS) + .withLang(LANGUAGE) + .withElapsedSeconds(1L) + .withDigest(BASE64_DIGEST) + .withRelyingPartyName(RELYING_PARTY_NAME); + + var exception = assertThrows(SmartIdClientException.class, () -> builder.buildDeviceLink(sessionSecret)); + + assertEquals("Parameter 'sessionSecret' cannot be empty", exception.getMessage()); + } + } + + private static Map toQueryParamsMap(URI uri) { + return Arrays.stream(uri.getQuery().split("&")) + .map(s -> s.split("=")) + .collect(Collectors.toMap(s -> s[0], s -> s[1])); + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java index 38e6a8b0..91c63e40 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkCertificateChoiceSessionRequestBuilderTest.java @@ -1,278 +1,278 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.util.Set; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; - -class DeviceLinkCertificateChoiceSessionRequestBuilderTest { - - private SmartIdConnector connector; - private DeviceLinkCertificateChoiceSessionRequestBuilder builderService; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - - builderService = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withNonce("1234567890") - .withInitialCallbackUrl("https://example.com/callback"); - } - - @Test - void initiateCertificateChoice() { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullRequestProperties() { - builderService.withShareMdClientIpAddress(false); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - assertEquals("test-session-id", result.sessionID()); - assertEquals("test-session-token", result.sessionToken()); - assertEquals("test-session-secret", result.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); - - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_missingCertificateLevel() { - builderService.withCertificateLevel(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initiateCertificateChoice_withValidCapabilities(String[] capabilities, Set expectedCapabilities) { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - builderService.withCapabilities(capabilities).initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkCertificateChoiceSessionRequest.class); - verify(connector).initDeviceLinkCertificateChoice(requestCaptor.capture()); - DeviceLinkCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { - var response = new DeviceLinkSessionResponse(sessionId, - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link"), - null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { - var response = new DeviceLinkSessionResponse("test-session-id", - sessionToken, - "test-session-secret", - URI.create("https://example.com/device-link")); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - sessionSecret, - URI.create("https://example.com/device-link")); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - uriString == null ? null : URI.create(uriString)); - - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); - assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_userAccountNotFound() { - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); - - var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); - assertEquals(UserAccountNotFoundException.class, ex.getClass()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyUUID() { - builderService.withRelyingPartyUUID(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_missingRelyingPartyName() { - builderService.withRelyingPartyName(null); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { - builderService.withNonce(invalidNonce); - - var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); - assertEquals("Value for 'nonce' must have length between 1 and 30 characters", ex.getMessage()); - } - - @Test - void initiateCertificateChoice_withoutInitialCallbackUrl() { - builderService.withInitialCallbackUrl(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @Test - void initiateCertificateChoice_nullNonce() { - builderService.withNonce(null); - when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); - - DeviceLinkSessionResponse result = builderService.initCertificateChoice(); - - assertNotNull(result); - verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url) { - var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withNonce("123456") - .withInitialCallbackUrl(url); - - var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); - assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); - } - } - - private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { - return new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com"), - Arguments.of("https://example.com|test"), - Arguments.of("ftp://example.com") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; + +class DeviceLinkCertificateChoiceSessionRequestBuilderTest { + + private SmartIdConnector connector; + private DeviceLinkCertificateChoiceSessionRequestBuilder builderService; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + + builderService = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withNonce("1234567890") + .withInitialCallbackUrl("https://example.com/callback"); + } + + @Test + void initiateCertificateChoice() { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullRequestProperties() { + builderService.withShareMdClientIpAddress(false); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + assertEquals("test-session-id", result.sessionID()); + assertEquals("test-session-token", result.sessionToken()); + assertEquals("test-session-secret", result.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), result.deviceLinkBase()); + + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_missingCertificateLevel() { + builderService.withCertificateLevel(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initiateCertificateChoice_withValidCapabilities(String[] capabilities, Set expectedCapabilities) { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + builderService.withCapabilities(capabilities).initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkCertificateChoiceSessionRequest.class); + verify(connector).initDeviceLinkCertificateChoice(requestCaptor.capture()); + DeviceLinkCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionIDIsNullOrEmpty(String sessionId) { + var response = new DeviceLinkSessionResponse(sessionId, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link"), + null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionTokenIsNullOrEmpty(String sessionToken) { + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenSessionSecretIsNullOrEmpty(String sessionSecret) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initiateCertificateChoice_whenDeviceLinkBaseIsNullOrEmpty(String uriString) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + uriString == null ? null : URI.create(uriString)); + + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> builderService.initCertificateChoice()); + assertEquals("Device link certificate choice session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_userAccountNotFound() { + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenThrow(new UserAccountNotFoundException()); + + var ex = assertThrows(UserAccountNotFoundException.class, () -> builderService.initCertificateChoice()); + assertEquals(UserAccountNotFoundException.class, ex.getClass()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyUUID() { + builderService.withRelyingPartyUUID(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_missingRelyingPartyName() { + builderService.withRelyingPartyName(null); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initiateCertificateChoice_nonceWithInvalidLength(String invalidNonce) { + builderService.withNonce(invalidNonce); + + var ex = assertThrows(SmartIdClientException.class, () -> builderService.initCertificateChoice()); + assertEquals("Value for 'nonce' must have length between 1 and 30 characters", ex.getMessage()); + } + + @Test + void initiateCertificateChoice_withoutInitialCallbackUrl() { + builderService.withInitialCallbackUrl(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @Test + void initiateCertificateChoice_nullNonce() { + builderService.withNonce(null); + when(connector.initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class))).thenReturn(mockCertificateChoiceResponse()); + + DeviceLinkSessionResponse result = builderService.initCertificateChoice(); + + assertNotNull(result); + verify(connector).initDeviceLinkCertificateChoice(any(DeviceLinkCertificateChoiceSessionRequest.class)); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initCertificateChoice_initialCallbackUrlIsInvalid_throwException(String url) { + var builder = new DeviceLinkCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withNonce("123456") + .withInitialCallbackUrl(url); + + var exception = assertThrows(SmartIdClientException.class, builder::initCertificateChoice); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); + } + } + + private static DeviceLinkSessionResponse mockCertificateChoiceResponse() { + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java index 60b8cd0d..4d1cb1e2 100644 --- a/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/DeviceLinkSignatureSessionRequestBuilderTest.java @@ -1,522 +1,522 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; - -class DeviceLinkSignatureSessionRequestBuilderTest { - - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_withSemanticsIdentifier() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - assertEquals("test-session-id", signatureSessionResponse.sessionID()); - assertEquals("test-session-token", signatureSessionResponse.sessionToken()); - assertEquals("test-session-secret", signatureSessionResponse.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signatureSessionResponse.deviceLinkBase()); - } - - @Test - void initSignatureSession_withDocumentNumber() { - String documentNumber = "PNOEE-31111111111-MOCK-Q"; - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b - .withSemanticsIdentifier(null) - .withDocumentNumber(documentNumber)); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signature); - assertEquals("test-session-id", signature.sessionID()); - assertEquals("test-session-token", signature.sessionToken()); - assertEquals("test-session-secret", signature.sessionSecret()); - assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initSignatureSession_withNonce_ok(String nonce) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - } - - @Test - void initSignatureSession_withRequestProperties() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.requestProperties()); - assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); - } - - @Test - void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); - - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signatureSessionResponse); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); - assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(mockSignatureSessionResponse()); - - DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertEquals("test-session-id", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.capabilities()); - } - - @Test - void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - assertNotNull(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); - verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @Test - void getSignatureSessionRequest_ok() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); - assertNotNull(signature); - - assertEquals("test-relying-party-uuid", deviceLinkSignatureSessionRequest.relyingPartyUUID()); - assertEquals("DEMO", deviceLinkSignatureSessionRequest.relyingPartyName()); - assertEquals("RAW_DIGEST_SIGNATURE", deviceLinkSignatureSessionRequest.signatureProtocol()); - assertNotNull(deviceLinkSignatureSessionRequest.signatureProtocolParameters()); - assertNotNull(deviceLinkSignatureSessionRequest.interactions()); - } - - @Test - void getSignatureSessionRequest_sessionNotStarted_throwException() { - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); - assertEquals("Signature session has not been initiated yet", ex.getMessage()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataWithHashAlgorithmSetToNull_throwsException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData("Test data".getBytes(), null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashWithHashAlgorithmSetToNull_throwsException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash("Test data".getBytes(), null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_whenSignableHashAndDataAreNull_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashBeingSetAfterSignableData_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> toBaseDeviceLinkSessionRequestBuilder() - .withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has already been set with SignableData.", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataBeingSetAfterSignableHash_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) - .withSignableHash(new SignableHash("Test data".getBytes())) - .withSignableData(new SignableData("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has already been set with SignableHash.", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) - void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInitialCallbackUrl(url)); - - var exception = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_whenInteractionsIsNullOrEmpty_throwException(List interactions) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_interactionsListWithNullValue_throwException() { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initSignatureSession_invalidNonce(String nonce) { - var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); - } - } - - @Nested - class ResponseValidationTests { - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionID(String sessionID) { - var response = new DeviceLinkSessionResponse(sessionID, - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionToken(String sessionToken) { - var response = new DeviceLinkSessionResponse("test-session-id", - sessionToken, - "test-session-secret", - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponseParameters_missingSessionSecret(String sessionSecret) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - sessionSecret, - URI.create("https://example.com/device-link")); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { - var response = new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); - var builder = toBaseDeviceLinkSessionRequestBuilder(); - when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); - } - } - - private DeviceLinkSignatureSessionRequestBuilder toDeviceLinkSignatureSessionRequestBuilder(UnaryOperator builder) { - var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); - return builder.apply(deviceLinkSessionRequestBuilder); - } - - private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestBuilder() { - return new DeviceLinkSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("test-relying-party-uuid") - .withRelyingPartyName("DEMO") - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document"))) - .withSignableData(new SignableData("Test data".getBytes())); - } - - private DeviceLinkSessionResponse mockSignatureSessionResponse() { - return new DeviceLinkSessionResponse("test-session-id", - "test-session-token", - "test-session-secret", - URI.create("https://example.com/device-link")); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, null), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } - - private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("http://example.com"), - Arguments.of("https://example.com|test"), - Arguments.of("ftp://example.com") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; + +class DeviceLinkSignatureSessionRequestBuilderTest { + + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_withSemanticsIdentifier() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + assertEquals("test-session-id", signatureSessionResponse.sessionID()); + assertEquals("test-session-token", signatureSessionResponse.sessionToken()); + assertEquals("test-session-secret", signatureSessionResponse.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signatureSessionResponse.deviceLinkBase()); + } + + @Test + void initSignatureSession_withDocumentNumber() { + String documentNumber = "PNOEE-31111111111-MOCK-Q"; + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), eq(documentNumber))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b + .withSemanticsIdentifier(null) + .withDocumentNumber(documentNumber)); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signature); + assertEquals("test-session-id", signature.sessionID()); + assertEquals("test-session-token", signature.sessionToken()); + assertEquals("test-session-secret", signature.sessionSecret()); + assertEquals(URI.create("https://example.com/device-link"), signature.deviceLinkBase()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + } + + @Test + void initSignatureSession_withRequestProperties() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(true)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.requestProperties()); + assertTrue(capturedRequest.requestProperties().shareMdClientIpAddress()); + } + + @Test + void initSignatureSession_withSignatureAlgorithm_setsCorrectAlgorithm() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableHash(HashAlgorithm hashAlgorithm) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(signableHash)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_withSignableData(HashAlgorithm hashAlgorithm) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var signableData = new SignableData("Test hash".getBytes(), hashAlgorithm); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)); + + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signatureSessionResponse); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + String expectedDigest = Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest("Test hash".getBytes(), hashAlgorithm)); + assertEquals(expectedDigest, capturedRequest.signatureProtocolParameters().digest()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockSignatureSessionResponse()); + + DeviceLinkSessionResponse response = deviceLinkSessionRequestBuilder.initSignatureSession(); + assertEquals("test-session-id", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities(String[] capabilities, Set expectedCapabilities) { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + } + + @Test + void initSignatureSession_withDefaultAlgorithmWhenNoSignatureAlgorithmSet() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + assertNotNull(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(DeviceLinkSignatureSessionRequest.class); + verify(connector).initDeviceLinkSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + DeviceLinkSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @Test + void getSignatureSessionRequest_ok() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + DeviceLinkSessionResponse signature = deviceLinkSessionRequestBuilder.initSignatureSession(); + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSessionRequestBuilder.getSignatureSessionRequest(); + assertNotNull(signature); + + assertEquals("test-relying-party-uuid", deviceLinkSignatureSessionRequest.relyingPartyUUID()); + assertEquals("DEMO", deviceLinkSignatureSessionRequest.relyingPartyName()); + assertEquals("RAW_DIGEST_SIGNATURE", deviceLinkSignatureSessionRequest.signatureProtocol()); + assertNotNull(deviceLinkSignatureSessionRequest.signatureProtocolParameters()); + assertNotNull(deviceLinkSignatureSessionRequest.interactions()); + } + + @Test + void getSignatureSessionRequest_sessionNotStarted_throwException() { + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockSignatureSessionResponse()); + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::getSignatureSessionRequest); + assertEquals("Signature session has not been initiated yet", ex.getMessage()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier(String documentNumber) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber).withSemanticsIdentifier(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set. Anonymous signing is not allowed", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashWithHashAlgorithmSetToNull_throwsException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash("Test data".getBytes(), null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_whenSignableHashAndDataAreNull_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdClientException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashBeingSetAfterSignableData_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> toBaseDeviceLinkSessionRequestBuilder() + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableData.", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataBeingSetAfterSignableHash_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withSignableHash(new SignableHash("Test data".getBytes())) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has already been set with SignableHash.", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidInitialCallbackUrlArgumentProvider.class) + void initSignatureSession_initialCallbackUrlIsInvalid_throwException(String url) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInitialCallbackUrl(url)); + + var exception = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'initialCallbackUrl' must match pattern ^https://[^|]+$ and must not contain unencoded vertical bars", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_whenInteractionsIsNullOrEmpty_throwException(List interactions) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_interactionsListWithNullValue_throwException() { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initSignatureSession_duplicateInteractions_shouldThrowException(List duplicateInteractions) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withInteractions(duplicateInteractions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyUUID(String relyingPartyUUID) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_missingRelyingPartyName(String relyingPartyName) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + var deviceLinkSessionRequestBuilder = toDeviceLinkSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, deviceLinkSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters.", ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionID(String sessionID) { + var response = new DeviceLinkSessionResponse(sessionID, + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionToken(String sessionToken) { + var response = new DeviceLinkSessionResponse("test-session-id", + sessionToken, + "test-session-secret", + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionToken' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponseParameters_missingSessionSecret(String sessionSecret) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + sessionSecret, + URI.create("https://example.com/device-link")); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'sessionSecret' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_deviceLinkBaseIsMissingOrBlank_throwException(String deviceLinkBaseValue) { + var response = new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + deviceLinkBaseValue == null ? null : URI.create(deviceLinkBaseValue)); + var builder = toBaseDeviceLinkSessionRequestBuilder(); + when(connector.initDeviceLinkSignature(any(DeviceLinkSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Device link signature session initialisation response field 'deviceLinkBase' is missing or empty", ex.getMessage()); + } + } + + private DeviceLinkSignatureSessionRequestBuilder toDeviceLinkSignatureSessionRequestBuilder(UnaryOperator builder) { + var deviceLinkSessionRequestBuilder = toBaseDeviceLinkSessionRequestBuilder(); + return builder.apply(deviceLinkSessionRequestBuilder); + } + + private DeviceLinkSignatureSessionRequestBuilder toBaseDeviceLinkSessionRequestBuilder() { + return new DeviceLinkSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("test-relying-party-uuid") + .withRelyingPartyName("DEMO") + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document"))) + .withSignableData(new SignableData("Test data".getBytes())); + } + + private DeviceLinkSessionResponse mockSignatureSessionResponse() { + return new DeviceLinkSessionResponse("test-session-id", + "test-session-token", + "test-session-secret", + URI.create("https://example.com/device-link")); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class InvalidInitialCallbackUrlArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("http://example.com"), + Arguments.of("https://example.com|test"), + Arguments.of("ftp://example.com") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java index f997fafb..aa6ad3f0 100644 --- a/src/test/java/ee/sk/smartid/DigestCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/DigestCalculatorTest.java @@ -1,79 +1,79 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.stream.Stream; - -import org.apache.commons.codec.binary.Hex; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -public class DigestCalculatorTest { - - private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); - - @ParameterizedTest - @ArgumentsSource(DigestAlgorithmValueProvider.class) - public void calculateDigest_sha256(HashAlgorithm hashAlgorithm, String expectedHex) { - byte[] sha = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, hashAlgorithm); - - assertThat(Hex.encodeHexString(sha), is(expectedHex)); - } - - @Test - public void calculateDigest_nullHashType() { - var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } - - private static class DigestAlgorithmValueProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(HashAlgorithm.SHA_256, "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"), - Arguments.of(HashAlgorithm.SHA_384, "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a"), - Arguments.of(HashAlgorithm.SHA_512, "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"), - Arguments.of(HashAlgorithm.SHA3_256, "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af"), - Arguments.of(HashAlgorithm.SHA3_384, "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c"), - Arguments.of(HashAlgorithm.SHA3_512, "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.apache.commons.codec.binary.Hex; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +public class DigestCalculatorTest { + + private static final byte[] HELLO_WORLD_BYTES = "Hello World!".getBytes(StandardCharsets.UTF_8); + + @ParameterizedTest + @ArgumentsSource(DigestAlgorithmValueProvider.class) + public void calculateDigest_sha256(HashAlgorithm hashAlgorithm, String expectedHex) { + byte[] sha = DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, hashAlgorithm); + + assertThat(Hex.encodeHexString(sha), is(expectedHex)); + } + + @Test + public void calculateDigest_nullHashType() { + var ex = assertThrows(SmartIdClientException.class, () -> DigestCalculator.calculateDigest(HELLO_WORLD_BYTES, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } + + private static class DigestAlgorithmValueProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(HashAlgorithm.SHA_256, "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"), + Arguments.of(HashAlgorithm.SHA_384, "bfd76c0ebbd006fee583410547c1887b0292be76d582d96c242d2a792723e3fd6fd061f9d5cfd13b8f961358e6adba4a"), + Arguments.of(HashAlgorithm.SHA_512, "861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8"), + Arguments.of(HashAlgorithm.SHA3_256, "d0e47486bbf4c16acac26f8b653592973c1362909f90262877089f9c8a4536af"), + Arguments.of(HashAlgorithm.SHA3_384, "f324cbd421326a2abaedf6f395d1a51e189d4a71c755f531289e519f079b224664961e385afcc37da348bd859f34fd1c"), + Arguments.of(HashAlgorithm.SHA3_512, "32400b5e89822de254e8d5d94252c52bdcb27a3562ca593e980364d9848b8041b98eabe16c1a6797484941d2376864a1b0e248b0f7af8b1555a778c336a5bf48") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java index a9fa46a7..36285640 100644 --- a/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java +++ b/src/test/java/ee/sk/smartid/DuplicateDeviceLinkInteractionsProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; - -public class DuplicateDeviceLinkInteractionsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - var interaction1 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); - var interaction2 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); - - return Stream.of( - Arguments.of(List.of(interaction1, interaction1)), - Arguments.of(List.of(interaction1, interaction2)) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; + +public class DuplicateDeviceLinkInteractionsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + var interaction1 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + var interaction2 = DeviceLinkInteraction.displayTextAndPin("Enter your PIN."); + + return Stream.of( + Arguments.of(List.of(interaction1, interaction1)), + Arguments.of(List.of(interaction1, interaction2)) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java index f55a85c8..b3d305cd 100644 --- a/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/DuplicateNotificationInteractionArgumentProvider.java @@ -1,49 +1,49 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; - -public class DuplicateNotificationInteractionArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - List.of(NotificationInteraction.displayTextAndPin("Enter your PIN."), - NotificationInteraction.displayTextAndPin("Enter your PIN.")), - List.of(NotificationInteraction.displayTextAndPin("Provide your PIN"), - NotificationInteraction.displayTextAndPin("Enter your PIN."))) - .map(Arguments::of); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; + +public class DuplicateNotificationInteractionArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + List.of(NotificationInteraction.displayTextAndPin("Enter your PIN."), + NotificationInteraction.displayTextAndPin("Enter your PIN.")), + List.of(NotificationInteraction.displayTextAndPin("Provide your PIN"), + NotificationInteraction.displayTextAndPin("Enter your PIN."))) + .map(Arguments::of); + } +} diff --git a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java index daf9906e..4e48ca24 100644 --- a/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java +++ b/src/test/java/ee/sk/smartid/ErrorResultHandlerTest.java @@ -1,131 +1,131 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionStatus; - -class ErrorResultHandlerTest { - - @Test - void handle_nullInput() { - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); - assertEquals("Parameter 'sessionResult' is not provided", smartIdClientException.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void handle_notOKEndResults(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionResult)); - } - - @ParameterizedTest - @ValueSource(strings = {"", "UNKNOWN"}) - void handle_unknownEndResult(String unknownEndResult) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(unknownEndResult); - - var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(sessionResult)); - assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); - } - - @Test - void handle_endResultIsUserRefusedInteraction_detailsMissing() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Details for refused interaction are missing", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void from_endResultIsUserRefusedInteraction_interactionIsEmpty(String interaction) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Details for refused interaction are missing", exception.getMessage()); - } - - @Test - void handle_endResultIsUserRefusedInteraction_interactionIsInvalidValue() { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction("invalid interaction"); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - assertEquals("Unexpected interaction type: invalid interaction", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void handle_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionStatus.getResult())); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionStatus; + +class ErrorResultHandlerTest { + + @Test + void handle_nullInput() { + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(null)); + assertEquals("Parameter 'sessionResult' is not provided", smartIdClientException.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void handle_notOKEndResults(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionResult)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "UNKNOWN"}) + void handle_unknownEndResult(String unknownEndResult) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(unknownEndResult); + + var smartIdClientException = assertThrows(SmartIdClientException.class, () -> ErrorResultHandler.handle(sessionResult)); + assertEquals("Unexpected session result: " + unknownEndResult, smartIdClientException.getMessage()); + } + + @Test + void handle_endResultIsUserRefusedInteraction_detailsMissing() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void from_endResultIsUserRefusedInteraction_interactionIsEmpty(String interaction) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Details for refused interaction are missing", exception.getMessage()); + } + + @Test + void handle_endResultIsUserRefusedInteraction_interactionIsInvalidValue() { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction("invalid interaction"); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + assertEquals("Unexpected interaction type: invalid interaction", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void handle_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> ErrorResultHandler.handle(sessionStatus.getResult())); + } +} diff --git a/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java index afbbea9b..a7395253 100644 --- a/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java +++ b/src/test/java/ee/sk/smartid/FileDefaultTrustedCAStoreBuilderTest.java @@ -1,102 +1,102 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class FileDefaultTrustedCAStoreBuilderTest { - - @Test - void validateTrustedCaCertificatesOnInitiation_ocspValidationsDisabled() { - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - assertFalse(trustedCACertStore.getTrustedCACertificates().isEmpty()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPathIsSetToEmpty_throwException(String path) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withTrustAnchorTruststorePath(path) - .build(); - }); - assertEquals("Trust anchor truststore path must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPasswordIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withTrustAnchorTruststorePassword(password) - .build(); - }); - assertEquals("Trust anchor truststore password must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePathIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withIntermediateCATruststorePath(password) - .build(); - }); - assertEquals("Intermediate CA certificate truststore path must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePasswordIsSetToEmpty_throwException(String password) { - var ex = assertThrows(SmartIdClientException.class, () -> { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .withIntermediateCATruststorePassword(password) - .build(); - }); - assertEquals("Intermediate CA certificate truststore password must be set", ex.getMessage()); - } - - @Disabled("Not yet implemented") - @Test - void validateTrustedCaCertificatesOnInitiation_withOCSPValidationTurnedOn() { - new FileTrustedCAStoreBuilder() - .withOcspEnabled(true).build(); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class FileDefaultTrustedCAStoreBuilderTest { + + @Test + void validateTrustedCaCertificatesOnInitiation_ocspValidationsDisabled() { + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + assertFalse(trustedCACertStore.getTrustedCACertificates().isEmpty()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPathIsSetToEmpty_throwException(String path) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePath(path) + .build(); + }); + assertEquals("Trust anchor truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_trustStoreAnchorPasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withTrustAnchorTruststorePassword(password) + .build(); + }); + assertEquals("Trust anchor truststore password must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePathIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePath(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore path must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateTrustedCaCertificatesOnInitiation_intermediateCaTruststorePasswordIsSetToEmpty_throwException(String password) { + var ex = assertThrows(SmartIdClientException.class, () -> { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .withIntermediateCATruststorePassword(password) + .build(); + }); + assertEquals("Intermediate CA certificate truststore password must be set", ex.getMessage()); + } + + @Disabled("Not yet implemented") + @Test + void validateTrustedCaCertificatesOnInitiation_withOCSPValidationTurnedOn() { + new FileTrustedCAStoreBuilder() + .withOcspEnabled(true).build(); + } +} diff --git a/src/test/java/ee/sk/smartid/FileUtil.java b/src/test/java/ee/sk/smartid/FileUtil.java index c32d7b5d..20ad20ae 100644 --- a/src/test/java/ee/sk/smartid/FileUtil.java +++ b/src/test/java/ee/sk/smartid/FileUtil.java @@ -1,55 +1,55 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; - -public final class FileUtil { - - private FileUtil() { - } - - public static String readFileToString(String fileName) { - return new String(readFileBytes(fileName), StandardCharsets.UTF_8); - } - - public static byte[] readFileBytes(String fileName) { - try { - ClassLoader classLoader = FileUtil.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull(resource, "File not found: " + fileName); - return Files.readAllBytes(Paths.get(resource.toURI())); - } catch (Exception e) { - throw new RuntimeException("Exception: " + e.getMessage(), e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public final class FileUtil { + + private FileUtil() { + } + + public static String readFileToString(String fileName) { + return new String(readFileBytes(fileName), StandardCharsets.UTF_8); + } + + public static byte[] readFileBytes(String fileName) { + try { + ClassLoader classLoader = FileUtil.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + return Files.readAllBytes(Paths.get(resource.toURI())); + } catch (Exception e) { + throw new RuntimeException("Exception: " + e.getMessage(), e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java index 737a141d..e3fc3a35 100644 --- a/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java +++ b/src/test/java/ee/sk/smartid/InvalidCertificateGenerator.java @@ -1,146 +1,146 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.math.BigInteger; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; -import java.security.SignatureException; -import java.security.cert.X509Certificate; -import java.util.Date; - -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.bouncycastle.jce.X509Principal; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.x509.X509V3CertificateGenerator; - -public final class InvalidCertificateGenerator { - - private InvalidCertificateGenerator() { - } - - public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { - ASN1EncodableVector vec = new ASN1EncodableVector(); - vec.addAll(policyInformations); - return CertificatePolicies.getInstance(new DERSequence(vec)); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private CertificatePolicies policies; - private ExtendedKeyUsage extendedKeyUsage; - private KeyUsage keyUsage; - private QCStatement qcStatement; - - public Builder withPolicies(CertificatePolicies policies) { - this.policies = policies; - return this; - } - - public Builder withExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) { - this.extendedKeyUsage = extendedKeyUsage; - return this; - } - - public Builder withKeyUsage(KeyUsage keyUsage) { - this.keyUsage = keyUsage; - return this; - } - - public Builder withQcStatement(QCStatement qcStatement) { - this.qcStatement = qcStatement; - return this; - } - - public X509Certificate createCertificate() { - Security.addProvider(new BouncyCastleProvider()); - KeyPair kp = createKeyPair(); - X509V3CertificateGenerator certGen = getBaseX509Generator(kp); - if (policies != null) { - certGen.addExtension(Extension.certificatePolicies, false, policies); - } - if (extendedKeyUsage != null) { - certGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); - } - if (keyUsage != null) { - certGen.addExtension(Extension.keyUsage, true, keyUsage); - } - if (qcStatement != null) { - certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); - } - return generate(certGen, kp); - } - - private static KeyPair createKeyPair() { - try { - KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); - kpg.initialize(2048); - return kpg.generateKeyPair(); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new RuntimeException(e); - } - } - - private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { - X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); - X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); - - X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setIssuerDN(issuer); - certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); - certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); - certGen.setSubjectDN(subject); - certGen.setPublicKey(kp.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen; - } - - private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { - try { - return certGen.generateX509Certificate(kp.getPrivate(), "BC"); - } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { - throw new RuntimeException(e); - } - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.x509.X509V3CertificateGenerator; + +public final class InvalidCertificateGenerator { + + private InvalidCertificateGenerator() { + } + + public static CertificatePolicies createCertificatePolicies(PolicyInformation... policyInformations) { + ASN1EncodableVector vec = new ASN1EncodableVector(); + vec.addAll(policyInformations); + return CertificatePolicies.getInstance(new DERSequence(vec)); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private CertificatePolicies policies; + private ExtendedKeyUsage extendedKeyUsage; + private KeyUsage keyUsage; + private QCStatement qcStatement; + + public Builder withPolicies(CertificatePolicies policies) { + this.policies = policies; + return this; + } + + public Builder withExtendedKeyUsage(ExtendedKeyUsage extendedKeyUsage) { + this.extendedKeyUsage = extendedKeyUsage; + return this; + } + + public Builder withKeyUsage(KeyUsage keyUsage) { + this.keyUsage = keyUsage; + return this; + } + + public Builder withQcStatement(QCStatement qcStatement) { + this.qcStatement = qcStatement; + return this; + } + + public X509Certificate createCertificate() { + Security.addProvider(new BouncyCastleProvider()); + KeyPair kp = createKeyPair(); + X509V3CertificateGenerator certGen = getBaseX509Generator(kp); + if (policies != null) { + certGen.addExtension(Extension.certificatePolicies, false, policies); + } + if (extendedKeyUsage != null) { + certGen.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); + } + if (keyUsage != null) { + certGen.addExtension(Extension.keyUsage, true, keyUsage); + } + if (qcStatement != null) { + certGen.addExtension(Extension.qCStatements, false, new DERSequence(qcStatement)); + } + return generate(certGen, kp); + } + + private static KeyPair createKeyPair() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + private static X509V3CertificateGenerator getBaseX509Generator(KeyPair kp) { + X509Principal issuer = new X509Principal("CN=MyRootCA, O=MyOrg, C=US"); + X509Principal subject = new X509Principal("CN=TestCert, O=MyOrg, C=US"); + + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setIssuerDN(issuer); + certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60)); + certGen.setNotAfter(new Date(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)); + certGen.setSubjectDN(subject); + certGen.setPublicKey(kp.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return certGen; + } + + private static X509Certificate generate(X509V3CertificateGenerator certGen, KeyPair kp) { + try { + return certGen.generateX509Certificate(kp.getPrivate(), "BC"); + } catch (NoSuchProviderException | SignatureException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java index dec1f549..fabc18c2 100644 --- a/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java +++ b/src/test/java/ee/sk/smartid/InvalidRpChallengeArgumentProvider.java @@ -1,50 +1,50 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.bouncycastle.util.encoders.Base64.*; - -import java.util.stream.Stream; - -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -public class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), - "Value for 'rpChallenge' must be Base64-encoded string"), - Arguments.of(Named.of("provided value sizes is less than allowed", toBase64String("a".repeat(30).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters"), - Arguments.of(Named.of("provided value sizes exceeds max range value", toBase64String("a".repeat(67).getBytes())), - "Value for 'rpChallenge' must have length between 44 and 88 characters") - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.bouncycastle.util.encoders.Base64.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +public class InvalidRpChallengeArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("provided string is not in Base64 format", "invalid value"), + "Value for 'rpChallenge' must be Base64-encoded string"), + Arguments.of(Named.of("provided value sizes is less than allowed", toBase64String("a".repeat(30).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters"), + Arguments.of(Named.of("provided value sizes exceeds max range value", toBase64String("a".repeat(67).getBytes())), + "Value for 'rpChallenge' must have length between 44 and 88 characters") + ); + } +} diff --git a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java index 8de5f459..2e817836 100644 --- a/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/LinkedNotificationSignatureSessionRequestBuilderTest.java @@ -1,258 +1,258 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; - -class LinkedNotificationSignatureSessionRequestBuilderTest { - - private static final String DOCUMENT_NUMBER = "PNOEE-12345678901-MOCK-Q"; - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_ok() { - LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @ParameterizedTest - @EnumSource(CertificateLevel.class) - void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel certificateLevel) { - LinkedNotificationSignatureSessionRequestBuilder builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withCertificateLevel(certificateLevel) - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); - verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - LinkedSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { - LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) - .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); - - LinkedSignatureSessionResponse response = builder.initSignatureSession(); - - assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); - verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - LinkedSignatureSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedRequestCapabilities, request.capabilities()); - } - - @Nested - class ValidateRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_documentNumberIsEmpty_throwException(String documentNumber) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataOrSignableHashNotProvided_throwException() { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with SignableData or with SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAlreadyUsedForSettingDigest_throwException() { - var builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> builder.withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512)))); - assertEquals("Value for 'digestInput' has been already set with SignableData", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException() { - var builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber(DOCUMENT_NUMBER); - - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> builder.withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSignableData(new SignableData("Test data".getBytes()))); - assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withLinkedSessionID(linkedSessionID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'linkedSessionID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"1234567890123456789012345678901", ""}) - void initSignatureSession_nonceWithIncorrectLengthProvided_throwException(String nonce) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'nonce' must be 1-30 characters long", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_interactionsInEmpty_throwException(List interactions) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) - void initSignatureSession_interactionsContainDuplicates_throwException(List interactions) { - var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> - b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - } - - @Test - void initSignatureSession_sessionIDMissingFromResponse_throwException() { - LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); - when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse(null)); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Linked notification-base signature session response field 'sessionID' is missing or empty", ex.getMessage()); - } - - private LinkedNotificationSignatureSessionRequestBuilder toLinkedNotificationSignatureSessionRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseLinkedNotificationSignatureSessionRequestBuilder()); - } - - private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificationSignatureSessionRequestBuilder() { - return new LinkedNotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withLinkedSessionID("10000000-0000-0000-0000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; + +class LinkedNotificationSignatureSessionRequestBuilderTest { + + private static final String DOCUMENT_NUMBER = "PNOEE-12345678901-MOCK-Q"; + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_ok() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @ParameterizedTest + @EnumSource(CertificateLevel.class) + void initSignatureSession_withDifferentCertificateLevels_ok(CertificateLevel certificateLevel) { + LinkedNotificationSignatureSessionRequestBuilder builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withCertificateLevel(certificateLevel) + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + LinkedNotificationSignatureSessionRequestBuilder builder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))) + .thenReturn(new LinkedSignatureSessionResponse("20000000-0000-0000-0000-000000000000")); + + LinkedSignatureSessionResponse response = builder.initSignatureSession(); + + assertEquals("20000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(LinkedSignatureSessionRequest.class); + verify(connector).initLinkedNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + LinkedSignatureSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + + @Nested + class ValidateRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_documentNumberIsEmpty_throwException(String documentNumber) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(documentNumber)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'documentNumber' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataOrSignableHashNotProvided_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with SignableData or with SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAlreadyUsedForSettingDigest_throwException() { + var builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512)))); + assertEquals("Value for 'digestInput' has been already set with SignableData", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashAlreadyUsedForSettingDigest_throwException() { + var builder = new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER); + + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> builder.withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes()))); + assertEquals("Value for 'digestInput' has been already set with SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_linkedSessionIDIsEmpty_throwException(String linkedSessionID) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withLinkedSessionID(linkedSessionID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'linkedSessionID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"1234567890123456789012345678901", ""}) + void initSignatureSession_nonceWithIncorrectLengthProvided_throwException(String nonce) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'nonce' must be 1-30 characters long", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_interactionsInEmpty_throwException(List interactions) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateDeviceLinkInteractionsProvider.class) + void initSignatureSession_interactionsContainDuplicates_throwException(List interactions) { + var linkedNotificationSignatureSessionRequestBuilder = toLinkedNotificationSignatureSessionRequestBuilder(b -> + b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, linkedNotificationSignatureSessionRequestBuilder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + } + + @Test + void initSignatureSession_sessionIDMissingFromResponse_throwException() { + LinkedNotificationSignatureSessionRequestBuilder builder = toBaseLinkedNotificationSignatureSessionRequestBuilder(); + when(connector.initLinkedNotificationSignature(any(LinkedSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(new LinkedSignatureSessionResponse(null)); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Linked notification-base signature session response field 'sessionID' is missing or empty", ex.getMessage()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toLinkedNotificationSignatureSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseLinkedNotificationSignatureSessionRequestBuilder()); + } + + private LinkedNotificationSignatureSessionRequestBuilder toBaseLinkedNotificationSignatureSessionRequestBuilder() { + return new LinkedNotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withLinkedSessionID("10000000-0000-0000-0000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))); + } +} diff --git a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java index d9345579..26622f2c 100644 --- a/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NonQualifiedSignatureCertificatePurposeValidatorTest.java @@ -1,120 +1,120 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.Security; -import java.security.cert.X509Certificate; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class NonQualifiedSignatureCertificatePurposeValidatorTest { - - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; - private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; - - private NonQualifiedSignatureCertificatePurposeValidator validator; - - @BeforeEach - void setUp() { - Security.addProvider(new BouncyCastleProvider()); - validator = new NonQualifiedSignatureCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) - void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { - PolicyInformation skNQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_POLICY_OID), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); - assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); - } - - private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class NonQualifiedSignatureCertificatePurposeValidatorTest { + + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; + + private NonQualifiedSignatureCertificatePurposeValidator validator; + + @BeforeEach + void setUp() { + Security.addProvider(new BouncyCastleProvider()); + validator = new NonQualifiedSignatureCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(NQ_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skNQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skNQPolicy, ncpPolicy); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(certificate)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java index 52c34e1f..8ae7b76f 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationResponseValidatorTest.java @@ -1,204 +1,204 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; - -class NotificationAuthenticationResponseValidatorTest { - - private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt"); - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - private static final String SIGNATURE_VALUE = "DR6pERkYxg5+pa7c0675yEmithtHzEnsqMyOD7RgZlwJgyR/z7VBxOZOUxdakjkT2LK6Jfo3RqxMeYciGfidieJ6vbdyzLDoSnrreaJguo1W5n5blz6Zqb+bkum/30qex7S31ubmRnNM/yIIVJ+/uuAgZgoQIwUV/KmTOE+0GEWFbqvxqFr7BkfrMX4luRrzfXkzpiWqlO8DXoQq4zfo3c00JsvCiM7PSfK4TLVR1FldXmGiV4ftcIep+YoPIxzzIbToyZ0+XYLIgobBio3EHyp2Z3rEWjASfY7+27c0TLkx8gRchxUcowxepioS49lz0trhMzbxNe1NCskHUAa3oodIH0xPNVD/B03uEKziK0r8mGWanHFvOhlqxnCfeN3AuQi5BJ0X7oybMWEvJ06dHlRBc3LrKhM1RrKkSiMy/eI0lTXDajJPupp7Zq/Ck41GbFnn52woFwYAB0hP2kUf7patya9C5C4QyeWB7SnRqtWTXprOMlPHG/KAjh7d61BhjV94zrFKj6YHcDxoQ6a31laYuyhkPMhqdzui1E/4BhWNiJsMkiqdB++VEgL5eT/76xHQuHIUD4GXHmAJnsQjBjFx5ws/yl5pFWsc/GR5H5oNT73Iaw2WSPReXLr7ZD8XEWmTV/GhjXoRUoEjtJrEIv30dYjXqE9Kv+B89tVk2gPHutgNuJJwwoZUaP61ym9w3WawR7ElJ3A8lvYjBPPOY3nYK/hu10imk/9cjdBJaNnMAlfsyzaXtBwBqdu5d80ibFAXkQ9aLwkqURX/Xnmw+lXIzj+p4T2BzhaGR7994qCVksoWPP/0xdvO+lYDM0YLPTvZTXN2PZVgt9NqYTEZHG6/4bcGoIkDTutAxF859rHBplzlMOGDz+sZPKHnLrKMnWaSaSbCVHi7pwF2vcq6QxkzY0grRAKYmmObPP7ORhIjXt5ENoW6n5CptgowizS4CckiaAe0u3QtMp+NoGYg/LSeef7NFhDDf8tUK0azHlAUDb3HPGUtQ3dvYX3JlCoX"; - - private NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - notificationAuthenticationResponseValidator = NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - } - - @Test - void validate_ok() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", SIGNATURE_VALUE); - - AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Nested - class ValidateInputs { - - @Test - void validate_sessionStatusNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_authenticationSessionRequestIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); - assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_emptySchemaNameIsProvided_throwException(String schemaName) { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); - assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); - } - } - - @Test - void validate_sessionStatusResultIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); - assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); - } - - @Nested - class ValidateSessionStatusCertificate { - - @Test - void validate_certificateLevelLowerThanRequested_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", SIGNATURE_VALUE); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", SIGNATURE_VALUE); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - } - - @Nested - class ValidateAuthenticationSignature { - - @Test - void validate_invalidSignature_throwException() { - var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); - - assertEquals("Signature value validation failed", ex.getMessage()); - } - } - - private static NotificationAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { - return new NotificationAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel, - SignatureProtocol.ACSP_V2.name(), - new AcspV2SignatureProtocolParameters("3mhDkd0ulDR/WVZx678FcrNw4pUhrZxcQsmejf8jQ1HtSp3GAxCH/Fi9EEiuULp44G/KNKONPXZELqCSZw4AoA==", - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), - "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB3aXRoIFNtYXJ0LUlEIGRlbW8/In1d", - null, - null, - "numeric4"); - } - - private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { - var result = new SessionResult(); - result.setEndResult("OK"); - result.setDocumentNumber("PNOEE-40504040001-DEMO-Q"); - - var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); - sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - - SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); - maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); - maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); - - var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); - sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); - sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); - sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); - sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); - - var signature = new SessionSignature(); - signature.setServerRandom("9eZeWMTJ9YYBtjj5jK8p1sLm"); - signature.setUserChallenge("RvrVNS1GJYCsuEnEqPCdHHn5vl65F3XiBjmxB4zSosw"); - signature.setValue(signatureValue); - signature.setFlowType(FlowType.NOTIFICATION.getDescription()); - signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); - signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); - - var cert = new SessionCertificate(); - cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); - cert.setCertificateLevel(certificateLevel); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(result); - sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); - sessionStatus.setSignature(signature); - sessionStatus.setCert(cert); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - return sessionStatus; - } - - private static String toBase64(String data) { - return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); - } - -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; + +class NotificationAuthenticationResponseValidatorTest { + + private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt"); + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + private static final String SIGNATURE_VALUE = "DR6pERkYxg5+pa7c0675yEmithtHzEnsqMyOD7RgZlwJgyR/z7VBxOZOUxdakjkT2LK6Jfo3RqxMeYciGfidieJ6vbdyzLDoSnrreaJguo1W5n5blz6Zqb+bkum/30qex7S31ubmRnNM/yIIVJ+/uuAgZgoQIwUV/KmTOE+0GEWFbqvxqFr7BkfrMX4luRrzfXkzpiWqlO8DXoQq4zfo3c00JsvCiM7PSfK4TLVR1FldXmGiV4ftcIep+YoPIxzzIbToyZ0+XYLIgobBio3EHyp2Z3rEWjASfY7+27c0TLkx8gRchxUcowxepioS49lz0trhMzbxNe1NCskHUAa3oodIH0xPNVD/B03uEKziK0r8mGWanHFvOhlqxnCfeN3AuQi5BJ0X7oybMWEvJ06dHlRBc3LrKhM1RrKkSiMy/eI0lTXDajJPupp7Zq/Ck41GbFnn52woFwYAB0hP2kUf7patya9C5C4QyeWB7SnRqtWTXprOMlPHG/KAjh7d61BhjV94zrFKj6YHcDxoQ6a31laYuyhkPMhqdzui1E/4BhWNiJsMkiqdB++VEgL5eT/76xHQuHIUD4GXHmAJnsQjBjFx5ws/yl5pFWsc/GR5H5oNT73Iaw2WSPReXLr7ZD8XEWmTV/GhjXoRUoEjtJrEIv30dYjXqE9Kv+B89tVk2gPHutgNuJJwwoZUaP61ym9w3WawR7ElJ3A8lvYjBPPOY3nYK/hu10imk/9cjdBJaNnMAlfsyzaXtBwBqdu5d80ibFAXkQ9aLwkqURX/Xnmw+lXIzj+p4T2BzhaGR7994qCVksoWPP/0xdvO+lYDM0YLPTvZTXN2PZVgt9NqYTEZHG6/4bcGoIkDTutAxF859rHBplzlMOGDz+sZPKHnLrKMnWaSaSbCVHi7pwF2vcq6QxkzY0grRAKYmmObPP7ORhIjXt5ENoW6n5CptgowizS4CckiaAe0u3QtMp+NoGYg/LSeef7NFhDDf8tUK0azHlAUDb3HPGUtQ3dvYX3JlCoX"; + + private NotificationAuthenticationResponseValidator notificationAuthenticationResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().withOcspEnabled(false).build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + notificationAuthenticationResponseValidator = NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + } + + @Test + void validate_ok() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", SIGNATURE_VALUE); + + AuthenticationIdentity authenticationIdentity = notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Nested + class ValidateInputs { + + @Test + void validate_sessionStatusNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(null, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_authenticationSessionRequestIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), null, "smart-id-demo", null)); + assertEquals("Parameter 'authenticationSessionRequest' is not provided", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_emptySchemaNameIsProvided_throwException(String schemaName) { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), schemaName, null)); + assertEquals("Parameter 'schemaName' is not provided", ex.getMessage()); + } + } + + @Test + void validate_sessionStatusResultIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> notificationAuthenticationResponseValidator.validate(new SessionStatus(), toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo", null)); + assertEquals("Authentication session status field 'result' is empty", ex.getMessage()); + } + + @Nested + class ValidateSessionStatusCertificate { + + @Test + void validate_certificateLevelLowerThanRequested_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "ADVANCED", SIGNATURE_VALUE); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var sessionStatus = toSessionsStatus(SIGN_CERT, "QUALIFIED", SIGNATURE_VALUE); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + } + + @Nested + class ValidateAuthenticationSignature { + + @Test + void validate_invalidSignature_throwException() { + var sessionStatus = toSessionsStatus(AUTH_CERT, "QUALIFIED", toBase64("invalidSignature")); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> notificationAuthenticationResponseValidator.validate(sessionStatus, toAuthenticationSessionRequest("QUALIFIED"), "smart-id-demo")); + + assertEquals("Signature value validation failed", ex.getMessage()); + } + } + + private static NotificationAuthenticationSessionRequest toAuthenticationSessionRequest(String certificateLevel) { + return new NotificationAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel, + SignatureProtocol.ACSP_V2.name(), + new AcspV2SignatureProtocolParameters("3mhDkd0ulDR/WVZx678FcrNw4pUhrZxcQsmejf8jQ1HtSp3GAxCH/Fi9EEiuULp44G/KNKONPXZELqCSZw4AoA==", + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())), + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB3aXRoIFNtYXJ0LUlEIGRlbW8/In1d", + null, + null, + "numeric4"); + } + + private static SessionStatus toSessionsStatus(String certificateValue, String certificateLevel, String signatureValue) { + var result = new SessionResult(); + result.setEndResult("OK"); + result.setDocumentNumber("PNOEE-40504040001-DEMO-Q"); + + var sessionMaskGenAlgorithmParameters = new SessionMaskGenAlgorithmParameters(); + sessionMaskGenAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + + SessionMaskGenAlgorithm maskGenAlgorithm = new SessionMaskGenAlgorithm(); + maskGenAlgorithm.setAlgorithm(MaskGenAlgorithm.ID_MGF1.getAlgorithmName()); + maskGenAlgorithm.setParameters(sessionMaskGenAlgorithmParameters); + + var sessionSignatureAlgorithmParameters = new SessionSignatureAlgorithmParameters(); + sessionSignatureAlgorithmParameters.setHashAlgorithm(HashAlgorithm.SHA3_512.getAlgorithmName()); + sessionSignatureAlgorithmParameters.setTrailerField(TrailerField.BC.getValue()); + sessionSignatureAlgorithmParameters.setSaltLength(HashAlgorithm.SHA3_512.getOctetLength()); + sessionSignatureAlgorithmParameters.setMaskGenAlgorithm(maskGenAlgorithm); + + var signature = new SessionSignature(); + signature.setServerRandom("9eZeWMTJ9YYBtjj5jK8p1sLm"); + signature.setUserChallenge("RvrVNS1GJYCsuEnEqPCdHHn5vl65F3XiBjmxB4zSosw"); + signature.setValue(signatureValue); + signature.setFlowType(FlowType.NOTIFICATION.getDescription()); + signature.setSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName()); + signature.setSignatureAlgorithmParameters(sessionSignatureAlgorithmParameters); + + var cert = new SessionCertificate(); + cert.setValue(CertificateUtil.getEncodedCertificateData(certificateValue)); + cert.setCertificateLevel(certificateLevel); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(result); + sessionStatus.setSignatureProtocol(SignatureProtocol.ACSP_V2.name()); + sessionStatus.setSignature(signature); + sessionStatus.setCert(cert); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + return sessionStatus; + } + + private static String toBase64(String data) { + return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java index 086f4627..dab26384 100644 --- a/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationAuthenticationSessionRequestBuilderTest.java @@ -1,385 +1,385 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class NotificationAuthenticationSessionRequestBuilderTest { - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initAuthenticationSession_withDocumentNumber_ok() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void initAuthenticationSession_withSemanticsIdentifier() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder( - b -> b.withDocumentNumber(null).withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - builder.initAuthenticationSession(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initAuthenticationSession_ipQueryingProvided_ok(boolean ipRequested) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource - void initAuthenticationSession_hashAlgorithm_ok(HashAlgorithm expectedHashAlgorithm) { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(expectedHashAlgorithm)); - - builder.initAuthenticationSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedHashAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toNotificationAuthenticationResponse()); - - NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(0, request.capabilities().size()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { - NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) - .thenReturn(toNotificationAuthenticationResponse()); - - NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); - verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); - NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); - assertEquals(expectedRequestCapabilities, request.capabilities()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) - void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_hashAlgorithmIsSetToNull_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_interactionsAreEmpty_throwException(List interactions) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) - void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); - } - - @Test - void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", exception.getMessage()); - } - - @Test - void initAuthenticationSession_documentNumberAndSemanticIdentifierAreBothProvided_throwException() { - NotificationAuthenticationSessionRequestBuilder builder = - toNotificationAuthenticationSessionRequestBuilder( - b -> b.withDocumentNumber("PNOEE-1234567890-MOCK-Q") - .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); - - var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(sessionId); - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); - assertEquals("Notification-based authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); - } - } - - @Test - void getAuthenticationSessionRequest_ok() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - builder.initAuthenticationSession(); - NotificationAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - assertAuthenticationSessionRequest(request); - } - - @Test - void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { - when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); - NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); - - var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); - assertEquals("Notification-based authentication session has not been initialized yet", ex.getMessage()); - } - - private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { - return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); - } - - private NotificationAuthenticationSessionRequestBuilder toBaseNotificationAuthenticationSessionRequestBuilder() { - return new NotificationAuthenticationSessionRequestBuilder(connector) - .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") - .withRelyingPartyName("DEMO") - .withRpChallenge(generateBase64String("a".repeat(32))) - .withInteractions(Collections.singletonList(NotificationInteraction.displayTextAndPin("Verify the code"))) - .withDocumentNumber("PNOEE-1234567890-MOCK-Q"); - } - - private NotificationAuthenticationSessionResponse toNotificationAuthenticationResponse() { - return new NotificationAuthenticationSessionResponse("00000000-0000-0000-0000-000000000000"); - } - - private static String generateBase64String(String text) { - return Base64.toBase64String(text.getBytes()); - } - - private static void assertAuthenticationSessionRequest(NotificationAuthenticationSessionRequest request) { - assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); - assertEquals("DEMO", request.relyingPartyName()); - assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); - assertNotNull(request.signatureProtocolParameters()); - assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); - assertNotNull(request.interactions()); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class NotificationAuthenticationSessionRequestBuilderTest { + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initAuthenticationSession_withDocumentNumber_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void initAuthenticationSession_withSemanticsIdentifier() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber(null).withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + builder.initAuthenticationSession(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initAuthenticationSession_ipQueryingProvided_ok(boolean ipRequested) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initAuthenticationSession_certificateLevel_ok(AuthenticationCertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_signatureAlgorithm_ok(SignatureAlgorithm signatureAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(signatureAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(signatureAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource + void initAuthenticationSession_hashAlgorithm_ok(HashAlgorithm expectedHashAlgorithm) { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(expectedHashAlgorithm)); + + builder.initAuthenticationSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedHashAlgorithm.getAlgorithmName(), request.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void initSignatureSession_withCapabilitiesSetToEmpty_ok(String capabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(0, request.capabilities().size()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedRequestCapabilities) { + NotificationAuthenticationSessionRequestBuilder builder = toNotificationAuthenticationSessionRequestBuilder(b -> b.withCapabilities(capabilities)); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))) + .thenReturn(toNotificationAuthenticationResponse()); + + NotificationAuthenticationSessionResponse response = builder.initAuthenticationSession(); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationAuthenticationSessionRequest.class); + verify(connector).initNotificationAuthentication(requestCaptor.capture(), any(String.class)); + NotificationAuthenticationSessionRequest request = requestCaptor.getValue(); + assertEquals(expectedRequestCapabilities, request.capabilities()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_rpChallengeIsEmpty_throwException(String rpChallenge) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'rpChallenge' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidRpChallengeArgumentProvider.class) + void initAuthenticationSession_rpChallengeIsInvalid_throwException(String rpChallenge, String expectedException) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withRpChallenge(rpChallenge)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initAuthenticationSession_signatureAlgorithmIsSetToNull_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'signatureAlgorithm' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_hashAlgorithmIsSetToNull_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withHashAlgorithm(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'hashAlgorithm' must be set", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_interactionsAreEmpty_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initAuthenticationSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", exception.getMessage()); + } + + @Test + void initAuthenticationSession_noDocumentNumberOrSemanticsIdentifier_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", exception.getMessage()); + } + + @Test + void initAuthenticationSession_documentNumberAndSemanticIdentifierAreBothProvided_throwException() { + NotificationAuthenticationSessionRequestBuilder builder = + toNotificationAuthenticationSessionRequestBuilder( + b -> b.withDocumentNumber("PNOEE-1234567890-MOCK-Q") + .withSemanticsIdentifier(new SemanticsIdentifier("PNOEE-48010010101"))); + + var exception = assertThrows(SmartIdClientException.class, builder::initAuthenticationSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var notificationAuthenticationSessionResponse = new NotificationAuthenticationSessionResponse(sessionId); + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(notificationAuthenticationSessionResponse); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initAuthenticationSession); + assertEquals("Notification-based authentication session initialisation response field 'sessionID' is missing or empty", exception.getMessage()); + } + } + + @Test + void getAuthenticationSessionRequest_ok() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + builder.initAuthenticationSession(); + NotificationAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + assertAuthenticationSessionRequest(request); + } + + @Test + void getAuthenticationSessionRequest_authenticationNotInitialized_throwsException() { + when(connector.initNotificationAuthentication(any(NotificationAuthenticationSessionRequest.class), any(String.class))).thenReturn(toNotificationAuthenticationResponse()); + NotificationAuthenticationSessionRequestBuilder builder = toBaseNotificationAuthenticationSessionRequestBuilder(); + + var ex = assertThrows(SmartIdClientException.class, builder::getAuthenticationSessionRequest); + assertEquals("Notification-based authentication session has not been initialized yet", ex.getMessage()); + } + + private NotificationAuthenticationSessionRequestBuilder toNotificationAuthenticationSessionRequestBuilder(UnaryOperator builder) { + return builder.apply(toBaseNotificationAuthenticationSessionRequestBuilder()); + } + + private NotificationAuthenticationSessionRequestBuilder toBaseNotificationAuthenticationSessionRequestBuilder() { + return new NotificationAuthenticationSessionRequestBuilder(connector) + .withRelyingPartyUUID("00000000-0000-0000-0000-000000000000") + .withRelyingPartyName("DEMO") + .withRpChallenge(generateBase64String("a".repeat(32))) + .withInteractions(Collections.singletonList(NotificationInteraction.displayTextAndPin("Verify the code"))) + .withDocumentNumber("PNOEE-1234567890-MOCK-Q"); + } + + private NotificationAuthenticationSessionResponse toNotificationAuthenticationResponse() { + return new NotificationAuthenticationSessionResponse("00000000-0000-0000-0000-000000000000"); + } + + private static String generateBase64String(String text) { + return Base64.toBase64String(text.getBytes()); + } + + private static void assertAuthenticationSessionRequest(NotificationAuthenticationSessionRequest request) { + assertEquals("00000000-0000-0000-0000-000000000000", request.relyingPartyUUID()); + assertEquals("DEMO", request.relyingPartyName()); + assertEquals(SignatureProtocol.ACSP_V2.name(), request.signatureProtocol()); + assertNotNull(request.signatureProtocolParameters()); + assertEquals("rsassa-pss", request.signatureProtocolParameters().signatureAlgorithm()); + assertNotNull(request.interactions()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(AuthenticationCertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(AuthenticationCertificateLevel.QUALIFIED, "QUALIFIED") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java index d8edb254..94e9cb1c 100644 --- a/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationCertificateChoiceSessionRequestBuilderTest.java @@ -1,270 +1,270 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -class NotificationCertificateChoiceSessionRequestBuilderTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initCertificateChoiceSession_withSemanticsIdentifier_ok() { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toBaseNotificationCertChoiceRequestBuilder() - .initCertificateChoice(); - - ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); - verify(connector).initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); - SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); - - assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); - } - - @Nested - class ValidateRequiredRequestParameters { - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedCertificateLevel) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCertificateLevel, request.certificateLevel()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initCertificateChoiceSession_nonce_ok(String nonce) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withNonce(nonce)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - } - - @Test - void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toBaseNotificationCertChoiceRequestBuilder().initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertNull(request.requestProperties()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initCertificateChoiceSession_ipQueryingSet_ok(boolean ipRequested) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertNotNull(request.requestProperties()); - assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(createCertificateChoiceSessionResponse()); - - toNotificationCertChoiceRequestBuilder(b -> b.withCapabilities(capabilities)) - .initCertificateChoice(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); - verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedCapabilities, request.capabilities()); - } - - @ParameterizedTest - @NullAndEmptySource - void initCertificateChoiceSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidNonceProvider.class) - void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withNonce(invalidNonce)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals(expectedException, exception.getMessage()); - } - - @Test - void initCertificateChoiceSession_semanticsIdentifierMissing_throwException() { - NotificationCertificateChoiceSessionRequestBuilder builder = - toNotificationCertChoiceRequestBuilder(b -> b.withSemanticsIdentifier(null)); - - var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); - assertEquals("Value for 'semanticIdentifier' must be set", exception.getMessage()); - } - } - - @Nested - class ValidateRequiredResponseParameters { - - @ParameterizedTest - @NullAndEmptySource - void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { - var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(sessionId); - when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); - NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); - - var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initCertificateChoice); - assertEquals("Notification-based certificate choice response field 'sessionID' is missing or empty", exception.getMessage()); - } - } - - private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { - return new NotificationCertificateChoiceSessionResponse("00000000-0000-0000-0000-000000000000"); - } - - private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { - return modifier.apply(toBaseNotificationCertChoiceRequestBuilder()); - } - - private NotificationCertificateChoiceSessionRequestBuilder toBaseNotificationCertChoiceRequestBuilder() { - return new NotificationCertificateChoiceSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, Named.of("expected certificate level", null)), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), - Arguments.of(CertificateLevel.QSCD, "QSCD") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } - - private static class InvalidNonceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Empty string as value", ""), "Value for 'nonce' length must be between 1 and 30 characters"), - Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Value for 'nonce' length must be between 1 and 30 characters") - ); - } - } +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +class NotificationCertificateChoiceSessionRequestBuilderTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initCertificateChoiceSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toBaseNotificationCertChoiceRequestBuilder() + .initCertificateChoice(); + + ArgumentCaptor semanticsIdentifierCaptor = ArgumentCaptor.forClass(SemanticsIdentifier.class); + verify(connector).initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), semanticsIdentifierCaptor.capture()); + SemanticsIdentifier capturedSemanticsIdentifier = semanticsIdentifierCaptor.getValue(); + + assertEquals("PNOEE-48010010101", capturedSemanticsIdentifier.getIdentifier()); + } + + @Nested + class ValidateRequiredRequestParameters { + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initCertificateChoiceSession_certificateLevel_ok(CertificateLevel certificateLevel, String expectedCertificateLevel) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCertificateLevel, request.certificateLevel()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initCertificateChoiceSession_nonce_ok(String nonce) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(nonce)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + } + + @Test + void initCertificateChoiceSession_ipQueryingNotUsed_doNotCreatedRequestProperties_ok() { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toBaseNotificationCertChoiceRequestBuilder().initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNull(request.requestProperties()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initCertificateChoiceSession_ipQueryingSet_ok(boolean ipRequested) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withShareMdClientIpAddress(ipRequested)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertNotNull(request.requestProperties()); + assertEquals(ipRequested, request.requestProperties().shareMdClientIpAddress()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initCertificateChoiceSession_capabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(createCertificateChoiceSessionResponse()); + + toNotificationCertChoiceRequestBuilder(b -> b.withCapabilities(capabilities)) + .initCertificateChoice(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationCertificateChoiceSessionRequest.class); + verify(connector).initNotificationCertificateChoice(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationCertificateChoiceSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedCapabilities, request.capabilities()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyUUIDIsEmpty_throwException(String relyingPartyUUID) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initCertificateChoiceSession_relyingPartyNameIsEmpty_throwException(String relyingPartyName) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'relyingPartyName' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidNonceProvider.class) + void initAuthenticationSession_nonceOutOfBounds_throwException(String invalidNonce, String expectedException) { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withNonce(invalidNonce)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals(expectedException, exception.getMessage()); + } + + @Test + void initCertificateChoiceSession_semanticsIdentifierMissing_throwException() { + NotificationCertificateChoiceSessionRequestBuilder builder = + toNotificationCertChoiceRequestBuilder(b -> b.withSemanticsIdentifier(null)); + + var exception = assertThrows(SmartIdRequestSetupException.class, builder::initCertificateChoice); + assertEquals("Value for 'semanticIdentifier' must be set", exception.getMessage()); + } + } + + @Nested + class ValidateRequiredResponseParameters { + + @ParameterizedTest + @NullAndEmptySource + void initAuthenticationSession_sessionIdIsNotPresentInTheResponse_throwException(String sessionId) { + var notificationCertificateChoiceSessionResponse = new NotificationCertificateChoiceSessionResponse(sessionId); + when(connector.initNotificationCertificateChoice(any(NotificationCertificateChoiceSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(notificationCertificateChoiceSessionResponse); + NotificationCertificateChoiceSessionRequestBuilder builder = toBaseNotificationCertChoiceRequestBuilder(); + + var exception = assertThrows(UnprocessableSmartIdResponseException.class, builder::initCertificateChoice); + assertEquals("Notification-based certificate choice response field 'sessionID' is missing or empty", exception.getMessage()); + } + } + + private NotificationCertificateChoiceSessionResponse createCertificateChoiceSessionResponse() { + return new NotificationCertificateChoiceSessionResponse("00000000-0000-0000-0000-000000000000"); + } + + private NotificationCertificateChoiceSessionRequestBuilder toNotificationCertChoiceRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationCertChoiceRequestBuilder()); + } + + private NotificationCertificateChoiceSessionRequestBuilder toBaseNotificationCertChoiceRequestBuilder() { + return new NotificationCertificateChoiceSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, Named.of("expected certificate level", null)), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } + + private static class InvalidNonceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Empty string as value", ""), "Value for 'nonce' length must be between 1 and 30 characters"), + Arguments.of(Named.of("Exceeded char length", "a".repeat(31)), "Value for 'nonce' length must be between 1 and 30 characters") + ); + } + } } \ No newline at end of file diff --git a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java index a785f1ef..55bbf052 100644 --- a/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java +++ b/src/test/java/ee/sk/smartid/NotificationSignatureSessionRequestBuilderTest.java @@ -1,496 +1,496 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; - -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.VerificationCode; - -class NotificationSignatureSessionRequestBuilderTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - private static final String DOCUMENT_NUMBER = "PNOEE-31111111111"; - - private SmartIdConnector connector; - - @BeforeEach - void setUp() { - connector = mock(SmartIdConnector.class); - } - - @Test - void initSignatureSession_withSemanticsIdentifier_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = toBaseNotificationSignatureSessionRequestBuilder().initSignatureSession(); - - assertSessionResponse(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(SEMANTICS_IDENTIFIER)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); - } - - @Test - void initSignatureSession_withDocumentNumber_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(mockNotificationSignatureSessionResponse()); - - NotificationSignatureSessionResponse signature = toNotificationSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(null).withDocumentNumber(DOCUMENT_NUMBER)) - .initSignatureSession(); - - assertSessionResponse(signature); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); - - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(CertificateLevelArgumentProvider.class) - void initSignatureSession_withCertificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(expectedValue, request.certificateLevel()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(ValidNonceArgumentSourceProvider.class) - void initSignatureSession_withNonce_ok(String nonce) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest request = requestCaptor.getValue(); - - assertEquals(nonce, request.nonce()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void initSignatureSession_withRequestProperties_ok(boolean shareIp) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))) - .thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(shareIp)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertNotNull(capturedRequest.requestProperties()); - assertEquals(shareIp, capturedRequest.requestProperties().shareMdClientIpAddress()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Test - void initSignatureSession_useDefaultHashAlgorithmForSignableHash_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableHash = new SignableHash("Test data".getBytes()); - - toNotificationSignatureSessionRequestBuilder(b -> b - .withSignableData(null) - .withSignableHash(signableHash)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_overrideDefaultHashAlgorithmForSignableHash_ok(HashAlgorithm hashAlgorithm) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); - - toNotificationSignatureSessionRequestBuilder(b -> b - .withSignableData(null) - .withSignableHash(signableHash)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Test - void initSignatureSession_useDefaultHashAlgorithmForSignableData_ok() { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableData = new SignableData("Test data".getBytes()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - } - - @ParameterizedTest - @EnumSource(HashAlgorithm.class) - void initSignatureSession_overrideDefaultHashAlgorithmForSignableData_ok(HashAlgorithm hashAlgorithm) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); - - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - - assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); - assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); - assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @ParameterizedTest - @ArgumentsSource(CapabilitiesArgumentProvider.class) - void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedCapabilities) { - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); - - toNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)) - .initSignatureSession(); - - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); - verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); - - NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); - assertEquals(expectedCapabilities, capturedRequest.capabilities()); - assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); - } - - @Nested - class ErrorCases { - - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyUUID_throwException(String relyingPartyUUID) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateParameters_missingRelyingPartyName_throwException(String relyingPartyName) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); - } - - @Test - void initSignatureSession_semanticIdentifierAndDocumentNumberAreBothSet_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(DOCUMENT_NUMBER).withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", ex.getMessage()); - } - - @Test - void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); - } - - @Test - void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - assertEquals("Value for 'digestInput' has already been set with SignableData", ex.getMessage()); - } - - @Test - void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, - () -> new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); - assertEquals("Value for 'digestInput' has already been set with SignableHash", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void initSignatureSession_interactionsAreNotProvided_throwException(List interactions) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); - } - - @Test - void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); - - var exception = assertThrows(SmartIdClientException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) - void initSignatureSession_duplicateInteractionsProvided_throwException(List interactions) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "1234567890123456789012345678901"}) - void initSignatureSession_invalidNonce(String nonce) { - NotificationSignatureSessionRequestBuilder builder = - toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); - - var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); - assertEquals("Value for 'nonce' length must be between 1 and 30 characters", ex.getMessage()); - } - } - - @Nested - class ResponseValidationTests { - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingSessionID_throwException(String sessionID) { - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(sessionID, new VerificationCode(null, null)); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'sessionID' is missing or empty", ex.getMessage()); - } - - @ParameterizedTest - @NullSource - void validateResponseParameters_missingVerificationCode_throwException(VerificationCode verificationCode) { - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingVerificationCodeType_throwException(String vcType) { - var verificationCode = new VerificationCode(vcType, null); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.type' is missing or empty", ex.getMessage()); - } - - @Test - void validateResponse_unsupportedVerificationCodeType_throwException() { - var verificationCode = new VerificationCode("unsupportedType", null); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.type' contains unsupported value", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateResponse_missingVerificationCodeValue_throwException(String vcValue) { - var verificationCode = new VerificationCode("numeric4", vcValue); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.value' is missing or empty", ex.getMessage()); - } - - @Test - void validateResponse_verificationCodeDoesNotMatchPattern_throwException() { - var verificationCode = new VerificationCode("numeric4", "aaaaaa"); - NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); - when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); - NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); - assertEquals("Notification-based signature response field 'vc.value' does not match the required pattern", ex.getMessage()); - } - } - - private NotificationSignatureSessionRequestBuilder toNotificationSignatureSessionRequestBuilder(UnaryOperator modifier) { - return modifier.apply(toBaseNotificationSignatureSessionRequestBuilder()); - } - - private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSessionRequestBuilder() { - return new NotificationSignatureSessionRequestBuilder(connector) - .withRelyingPartyUUID(RELYING_PARTY_UUID) - .withRelyingPartyName(RELYING_PARTY_NAME) - .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) - .withSignableData(new SignableData("Test data".getBytes())) - .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); - } - - private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { - var verificationCode = new VerificationCode("numeric4", "4927"); - return toNotificationSignatureSessionResponse(verificationCode); - } - - private static NotificationSignatureSessionResponse toNotificationSignatureSessionResponse(VerificationCode verificationCode) { - return new NotificationSignatureSessionResponse("00000000-0000-0000-0000-000000000000", verificationCode); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse signature) { - assertNotNull(signature); - assertEquals("00000000-0000-0000-0000-000000000000", signature.sessionID()); - assertEquals("numeric4", signature.vc().type()); - assertEquals("4927", signature.vc().value()); - } - - private static class CertificateLevelArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(null, null), - Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), - Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), - Arguments.of(CertificateLevel.QSCD, "QSCD") - ); - } - } - - private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; + +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.VerificationCode; + +class NotificationSignatureSessionRequestBuilderTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111"; + + private SmartIdConnector connector; + + @BeforeEach + void setUp() { + connector = mock(SmartIdConnector.class); + } + + @Test + void initSignatureSession_withSemanticsIdentifier_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(SEMANTICS_IDENTIFIER))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = toBaseNotificationSignatureSessionRequestBuilder().initSignatureSession(); + + assertSessionResponse(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(SEMANTICS_IDENTIFIER)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); + } + + @Test + void initSignatureSession_withDocumentNumber_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), eq(DOCUMENT_NUMBER))).thenReturn(mockNotificationSignatureSessionResponse()); + + NotificationSignatureSessionResponse signature = toNotificationSignatureSessionRequestBuilder(b -> b.withSemanticsIdentifier(null).withDocumentNumber(DOCUMENT_NUMBER)) + .initSignatureSession(); + + assertSessionResponse(signature); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), eq(DOCUMENT_NUMBER)); + + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), requestCaptor.getValue().signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CertificateLevelArgumentProvider.class) + void initSignatureSession_withCertificateLevel_ok(CertificateLevel certificateLevel, String expectedValue) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withCertificateLevel(certificateLevel)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(expectedValue, request.certificateLevel()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(ValidNonceArgumentSourceProvider.class) + void initSignatureSession_withNonce_ok(String nonce) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest request = requestCaptor.getValue(); + + assertEquals(nonce, request.nonce()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), request.signatureProtocol()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void initSignatureSession_withRequestProperties_ok(boolean shareIp) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))) + .thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withShareMdClientIpAddress(shareIp)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertNotNull(capturedRequest.requestProperties()); + assertEquals(shareIp, capturedRequest.requestProperties().shareMdClientIpAddress()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Test + void initSignatureSession_useDefaultHashAlgorithmForSignableHash_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableHash = new SignableHash("Test data".getBytes()); + + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_overrideDefaultHashAlgorithmForSignableHash_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableHash = new SignableHash("Test hash".getBytes(), hashAlgorithm); + + toNotificationSignatureSessionRequestBuilder(b -> b + .withSignableData(null) + .withSignableHash(signableHash)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString("Test hash".getBytes()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Test + void initSignatureSession_useDefaultHashAlgorithmForSignableData_ok() { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableData = new SignableData("Test data".getBytes()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(HashAlgorithm.SHA_512.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + } + + @ParameterizedTest + @EnumSource(HashAlgorithm.class) + void initSignatureSession_overrideDefaultHashAlgorithmForSignableData_ok(HashAlgorithm hashAlgorithm) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + var signableData = new SignableData("Test data".getBytes(), hashAlgorithm); + + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(signableData)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + + assertEquals(SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithm()); + assertEquals(Base64.getEncoder().encodeToString(signableData.calculateHash()), capturedRequest.signatureProtocolParameters().digest()); + assertEquals(hashAlgorithm.getAlgorithmName(), capturedRequest.signatureProtocolParameters().signatureAlgorithmParameters().hashAlgorithm()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @ParameterizedTest + @ArgumentsSource(CapabilitiesArgumentProvider.class) + void initSignatureSession_withCapabilities_ok(String[] capabilities, Set expectedCapabilities) { + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(mockNotificationSignatureSessionResponse()); + + toNotificationSignatureSessionRequestBuilder(b -> b.withCapabilities(capabilities)) + .initSignatureSession(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(NotificationSignatureSessionRequest.class); + verify(connector).initNotificationSignature(requestCaptor.capture(), any(SemanticsIdentifier.class)); + + NotificationSignatureSessionRequest capturedRequest = requestCaptor.getValue(); + assertEquals(expectedCapabilities, capturedRequest.capabilities()); + assertEquals(SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), capturedRequest.signatureProtocol()); + } + + @Nested + class ErrorCases { + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyUUID_throwException(String relyingPartyUUID) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyUUID(relyingPartyUUID)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyUUID' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateParameters_missingRelyingPartyName_throwException(String relyingPartyName) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withRelyingPartyName(relyingPartyName)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'relyingPartyName' cannot be empty", ex.getMessage()); + } + + @Test + void initSignatureSession_semanticIdentifierAndDocumentNumberAreBothSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(DOCUMENT_NUMBER).withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Only one of 'semanticsIdentifier' or 'documentNumber' may be set", ex.getMessage()); + } + + @Test + void initSignatureSession_missingDocumentNumberAndSemanticsIdentifier_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withDocumentNumber(null).withSemanticsIdentifier(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Either 'documentNumber' or 'semanticsIdentifier' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signatureAlgorithmIsSetToNull_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignatureAlgorithm(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'signatureAlgorithm' must be set", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAndSignableHashAreNotSet_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withSignableData(null).withSignableHash(null)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'digestInput' must be set with either SignableData or SignableHash", ex.getMessage()); + } + + @Test + void initSignatureSession_signableDataAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableData", ex.getMessage()); + } + + @Test + void initSignatureSession_signableHashAlreadySetAndSignableHashIsAlsoAdded_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, + () -> new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableHash(new SignableHash(DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER)); + assertEquals("Value for 'digestInput' has already been set with SignableHash", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void initSignatureSession_interactionsAreNotProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", ex.getMessage()); + } + + @Test + void initAuthenticationSession_interactionsIsListWithNullValue_throwException() { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(Collections.singletonList(null))); + + var exception = assertThrows(SmartIdClientException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot be empty", exception.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(DuplicateNotificationInteractionArgumentProvider.class) + void initSignatureSession_duplicateInteractionsProvided_throwException(List interactions) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withInteractions(interactions)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'interactions' cannot contain duplicate types", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", "1234567890123456789012345678901"}) + void initSignatureSession_invalidNonce(String nonce) { + NotificationSignatureSessionRequestBuilder builder = + toNotificationSignatureSessionRequestBuilder(b -> b.withNonce(nonce)); + + var ex = assertThrows(SmartIdRequestSetupException.class, builder::initSignatureSession); + assertEquals("Value for 'nonce' length must be between 1 and 30 characters", ex.getMessage()); + } + } + + @Nested + class ResponseValidationTests { + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingSessionID_throwException(String sessionID) { + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + NotificationSignatureSessionResponse response = new NotificationSignatureSessionResponse(sessionID, new VerificationCode(null, null)); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'sessionID' is missing or empty", ex.getMessage()); + } + + @ParameterizedTest + @NullSource + void validateResponseParameters_missingVerificationCode_throwException(VerificationCode verificationCode) { + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeType_throwException(String vcType) { + var verificationCode = new VerificationCode(vcType, null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' is missing or empty", ex.getMessage()); + } + + @Test + void validateResponse_unsupportedVerificationCodeType_throwException() { + var verificationCode = new VerificationCode("unsupportedType", null); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.type' contains unsupported value", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateResponse_missingVerificationCodeValue_throwException(String vcValue) { + var verificationCode = new VerificationCode("numeric4", vcValue); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' is missing or empty", ex.getMessage()); + } + + @Test + void validateResponse_verificationCodeDoesNotMatchPattern_throwException() { + var verificationCode = new VerificationCode("numeric4", "aaaaaa"); + NotificationSignatureSessionResponse response = toNotificationSignatureSessionResponse(verificationCode); + when(connector.initNotificationSignature(any(NotificationSignatureSessionRequest.class), any(SemanticsIdentifier.class))).thenReturn(response); + NotificationSignatureSessionRequestBuilder builder = toBaseNotificationSignatureSessionRequestBuilder(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::initSignatureSession); + assertEquals("Notification-based signature response field 'vc.value' does not match the required pattern", ex.getMessage()); + } + } + + private NotificationSignatureSessionRequestBuilder toNotificationSignatureSessionRequestBuilder(UnaryOperator modifier) { + return modifier.apply(toBaseNotificationSignatureSessionRequestBuilder()); + } + + private NotificationSignatureSessionRequestBuilder toBaseNotificationSignatureSessionRequestBuilder() { + return new NotificationSignatureSessionRequestBuilder(connector) + .withRelyingPartyUUID(RELYING_PARTY_UUID) + .withRelyingPartyName(RELYING_PARTY_NAME) + .withInteractions(List.of(NotificationInteraction.displayTextAndPin("Sign?"))) + .withSignableData(new SignableData("Test data".getBytes())) + .withSemanticsIdentifier(SEMANTICS_IDENTIFIER); + } + + private NotificationSignatureSessionResponse mockNotificationSignatureSessionResponse() { + var verificationCode = new VerificationCode("numeric4", "4927"); + return toNotificationSignatureSessionResponse(verificationCode); + } + + private static NotificationSignatureSessionResponse toNotificationSignatureSessionResponse(VerificationCode verificationCode) { + return new NotificationSignatureSessionResponse("00000000-0000-0000-0000-000000000000", verificationCode); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse signature) { + assertNotNull(signature); + assertEquals("00000000-0000-0000-0000-000000000000", signature.sessionID()); + assertEquals("numeric4", signature.vc().type()); + assertEquals("4927", signature.vc().value()); + } + + private static class CertificateLevelArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(null, null), + Arguments.of(CertificateLevel.ADVANCED, "ADVANCED"), + Arguments.of(CertificateLevel.QUALIFIED, "QUALIFIED"), + Arguments.of(CertificateLevel.QSCD, "QSCD") + ); + } + } + + private static class ValidNonceArgumentSourceProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, "a", "a".repeat(30)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java index 203cca33..f0c4560c 100644 --- a/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/QrCodeGeneratorTest.java @@ -1,210 +1,210 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URI; -import java.util.Base64; -import java.util.regex.Pattern; - -import javax.imageio.ImageIO; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class QrCodeGeneratorTest { - - private static final int WHITE_COLOR = -1; - - @Test - void generateDataUri_validateQrContent() { - URI uri = createUri(); - String base64ImageData = QrCodeGenerator.generateDataUri(uri.toString()); - - assertNotNull(base64ImageData); - String[] parts = base64ImageData.split(","); - assertEquals("data:image/png;base64", parts[0]); - assertEquals(uri.toString(), QrCodeUtil.extractQrContent(parts[1]).getText()); - } - - @Nested - class DefaultValues { - - @Test - void generateDataUri() { - URI uri = createUri(); - String qrDataUri = QrCodeGenerator.generateDataUri(uri.toString()); - String imgBase64 = qrDataUri.split(",")[1]; - BufferedImage qrImage = convertToBufferedImage(imgBase64); - - assertEquals(610, qrImage.getHeight()); - assertEquals(610, qrImage.getHeight()); - assertTrue(validateQuietArea(qrImage, 4, 10)); - assertQrModuleSize(qrImage, 4, 10); - } - - @Test - void generateBufferedImage() { - URI uri = createUri(); - BufferedImage qrImage = QrCodeGenerator.generateImage(uri.toString()); - - assertEquals(610, qrImage.getHeight()); - assertEquals(610, qrImage.getHeight()); - assertTrue(validateQuietArea(qrImage, 4, 10)); - assertQrModuleSize(qrImage, 4, 10); - } - } - - @Test - void generateImage_providedCustomValues() { - URI uri = createUri(); - int quietAreaSize = 2; - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 100, 100, quietAreaSize); - - assertEquals(100, bufferedImage.getHeight()); - assertEquals(100, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - float expectedModuleSize = (float) bufferedImage.getWidth() / (53 + 2 * quietAreaSize); - assertQrModuleSize(bufferedImage, 2, expectedModuleSize); - } - - @Nested - class QrCodeModulePixelRanges { - - @Test - void generateImage_providedCustomValues_moduleSize6px() { - URI uri = createUri(); - int quietAreaSize = 2; - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 366, 366, quietAreaSize); - - assertEquals(366, bufferedImage.getHeight()); - assertEquals(366, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - assertQrModuleSize(bufferedImage, 4, 6); - } - - @Test - void generateImage_providedCustomValues_moduleSize19px() { - URI uri = createUri(); - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 1159, 1159, 4); - - assertEquals(1159, bufferedImage.getHeight()); - assertEquals(1159, bufferedImage.getWidth()); - assertTrue(validateQuietArea(bufferedImage, 2, 1)); - - assertQrModuleSize(bufferedImage, 4, 19); - } - } - - @ParameterizedTest - @NullAndEmptySource - void generateImage_providedDataIsEmpty_throwException(String data) { - var ex = assertThrows(SmartIdClientException.class, () -> QrCodeGenerator.generateImage(data, 10, 10, 2)); - - assertEquals("Provided data cannot be empty", ex.getMessage()); - } - - @Test - void convertToBase64() { - URI uri = createUri(); - BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString()); - String base64ImageData = QrCodeGenerator.convertToDataUri(bufferedImage, "png"); - - String[] parts = base64ImageData.split(","); - assertEquals("data:image/png;base64", parts[0]); - Pattern pattern = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); - assertTrue(pattern.matcher(parts[1].replaceAll("\\s", "")).matches()); - } - - private static URI createUri() { - var linkBuilder = new DeviceLinkBuilder() - .withDeviceLinkBase("smartid://link") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") - .withElapsedSeconds(1L) - .withRelyingPartyName("DEMO") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions("interactions") - .withLang("ENG"); - - return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); - } - - private static BufferedImage convertToBufferedImage(String qrDataUri) { - byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); - try (ByteArrayInputStream bis = new ByteArrayInputStream(qrCodeBytes)) { - return ImageIO.read(bis); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static boolean validateQuietArea(BufferedImage qrImage, int quietZoneModules, int moduleSize) { - int quietZonePixelSize = quietZoneModules * moduleSize; - - // Validate top and bottom quiet areas - for (int y = 0; y < quietZonePixelSize; y++) { - for (int x = 0; x < qrImage.getWidth(); x++) { - if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(x, qrImage.getHeight() - 1 - y) != WHITE_COLOR) { - return false; - } - } - } - // Validate left and right quiet areas - for (int x = 0; x < quietZonePixelSize; x++) { - for (int y = 0; y < qrImage.getHeight(); y++) { - if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(qrImage.getWidth() - 1 - x, y) != WHITE_COLOR) { - return false; - } - } - } - return true; - } - - private static void assertQrModuleSize(BufferedImage qrImage, - int nrOfQuietAreaModules, - float expectedModuleSizePx) { - float qrCodeWidth = 53 * expectedModuleSizePx; - float quiteAreaWidth = nrOfQuietAreaModules * expectedModuleSizePx; - float expectedWidth = qrCodeWidth + 2 * quiteAreaWidth; - assertEquals(expectedWidth, qrImage.getWidth()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.Base64; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class QrCodeGeneratorTest { + + private static final int WHITE_COLOR = -1; + + @Test + void generateDataUri_validateQrContent() { + URI uri = createUri(); + String base64ImageData = QrCodeGenerator.generateDataUri(uri.toString()); + + assertNotNull(base64ImageData); + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + assertEquals(uri.toString(), QrCodeUtil.extractQrContent(parts[1]).getText()); + } + + @Nested + class DefaultValues { + + @Test + void generateDataUri() { + URI uri = createUri(); + String qrDataUri = QrCodeGenerator.generateDataUri(uri.toString()); + String imgBase64 = qrDataUri.split(",")[1]; + BufferedImage qrImage = convertToBufferedImage(imgBase64); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + + @Test + void generateBufferedImage() { + URI uri = createUri(); + BufferedImage qrImage = QrCodeGenerator.generateImage(uri.toString()); + + assertEquals(610, qrImage.getHeight()); + assertEquals(610, qrImage.getHeight()); + assertTrue(validateQuietArea(qrImage, 4, 10)); + assertQrModuleSize(qrImage, 4, 10); + } + } + + @Test + void generateImage_providedCustomValues() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 100, 100, quietAreaSize); + + assertEquals(100, bufferedImage.getHeight()); + assertEquals(100, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + float expectedModuleSize = (float) bufferedImage.getWidth() / (53 + 2 * quietAreaSize); + assertQrModuleSize(bufferedImage, 2, expectedModuleSize); + } + + @Nested + class QrCodeModulePixelRanges { + + @Test + void generateImage_providedCustomValues_moduleSize6px() { + URI uri = createUri(); + int quietAreaSize = 2; + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 366, 366, quietAreaSize); + + assertEquals(366, bufferedImage.getHeight()); + assertEquals(366, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 6); + } + + @Test + void generateImage_providedCustomValues_moduleSize19px() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString(), 1159, 1159, 4); + + assertEquals(1159, bufferedImage.getHeight()); + assertEquals(1159, bufferedImage.getWidth()); + assertTrue(validateQuietArea(bufferedImage, 2, 1)); + + assertQrModuleSize(bufferedImage, 4, 19); + } + } + + @ParameterizedTest + @NullAndEmptySource + void generateImage_providedDataIsEmpty_throwException(String data) { + var ex = assertThrows(SmartIdClientException.class, () -> QrCodeGenerator.generateImage(data, 10, 10, 2)); + + assertEquals("Provided data cannot be empty", ex.getMessage()); + } + + @Test + void convertToBase64() { + URI uri = createUri(); + BufferedImage bufferedImage = QrCodeGenerator.generateImage(uri.toString()); + String base64ImageData = QrCodeGenerator.convertToDataUri(bufferedImage, "png"); + + String[] parts = base64ImageData.split(","); + assertEquals("data:image/png;base64", parts[0]); + Pattern pattern = Pattern.compile("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); + assertTrue(pattern.matcher(parts[1].replaceAll("\\s", "")).matches()); + } + + private static URI createUri() { + var linkBuilder = new DeviceLinkBuilder() + .withDeviceLinkBase("smartid://link") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken("rTBfEhy0z4SlqmGHjIW6uQid") + .withElapsedSeconds(1L) + .withRelyingPartyName("DEMO") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions("interactions") + .withLang("ENG"); + + return linkBuilder.buildDeviceLink("B98ODiVCebRedSwdTk51zFSaGYyHtY1H2A0ocAi3/Ps="); + } + + private static BufferedImage convertToBufferedImage(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream bis = new ByteArrayInputStream(qrCodeBytes)) { + return ImageIO.read(bis); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static boolean validateQuietArea(BufferedImage qrImage, int quietZoneModules, int moduleSize) { + int quietZonePixelSize = quietZoneModules * moduleSize; + + // Validate top and bottom quiet areas + for (int y = 0; y < quietZonePixelSize; y++) { + for (int x = 0; x < qrImage.getWidth(); x++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(x, qrImage.getHeight() - 1 - y) != WHITE_COLOR) { + return false; + } + } + } + // Validate left and right quiet areas + for (int x = 0; x < quietZonePixelSize; x++) { + for (int y = 0; y < qrImage.getHeight(); y++) { + if (qrImage.getRGB(x, y) != WHITE_COLOR || qrImage.getRGB(qrImage.getWidth() - 1 - x, y) != WHITE_COLOR) { + return false; + } + } + } + return true; + } + + private static void assertQrModuleSize(BufferedImage qrImage, + int nrOfQuietAreaModules, + float expectedModuleSizePx) { + float qrCodeWidth = 53 * expectedModuleSizePx; + float quiteAreaWidth = nrOfQuietAreaModules * expectedModuleSizePx; + float expectedWidth = qrCodeWidth + 2 * quiteAreaWidth; + assertEquals(expectedWidth, qrImage.getWidth()); + } +} diff --git a/src/test/java/ee/sk/smartid/QrCodeUtil.java b/src/test/java/ee/sk/smartid/QrCodeUtil.java index 23ddb00c..5039bd55 100644 --- a/src/test/java/ee/sk/smartid/QrCodeUtil.java +++ b/src/test/java/ee/sk/smartid/QrCodeUtil.java @@ -1,69 +1,69 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.Base64; - -import javax.imageio.ImageIO; - -import com.google.zxing.BinaryBitmap; -import com.google.zxing.NotFoundException; -import com.google.zxing.Result; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.multi.qrcode.QRCodeMultiReader; - -public class QrCodeUtil { - - private QrCodeUtil() { - } - - public static Result extractQrContent(String qrDataUri) { - BinaryBitmap bitmap = getBinaryBitmap(qrDataUri); - Result result; - try { - result = Arrays.stream(new QRCodeMultiReader().decodeMultiple(bitmap)).findFirst().get(); - } catch (NotFoundException ex) { - throw new RuntimeException(ex); - } - return result; - } - - public static BinaryBitmap getBinaryBitmap(String qrDataUri) { - byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes)) { - BufferedImage bufferedImage = ImageIO.read(inputStream); - return new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; + +import javax.imageio.ImageIO; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.multi.qrcode.QRCodeMultiReader; + +public class QrCodeUtil { + + private QrCodeUtil() { + } + + public static Result extractQrContent(String qrDataUri) { + BinaryBitmap bitmap = getBinaryBitmap(qrDataUri); + Result result; + try { + result = Arrays.stream(new QRCodeMultiReader().decodeMultiple(bitmap)).findFirst().get(); + } catch (NotFoundException ex) { + throw new RuntimeException(ex); + } + return result; + } + + public static BinaryBitmap getBinaryBitmap(String qrDataUri) { + byte[] qrCodeBytes = Base64.getMimeDecoder().decode(qrDataUri); + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(qrCodeBytes)) { + BufferedImage bufferedImage = ImageIO.read(inputStream); + return new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(bufferedImage))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java index cf19d89a..2069e9b9 100644 --- a/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/QualifiedSignatureCertificatePurposeValidatorTest.java @@ -1,155 +1,155 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; -import org.bouncycastle.asn1.x509.qualified.QCStatement; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -class QualifiedSignatureCertificatePurposeValidatorTest { - - private static final String QUALIFIED_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); - private static final String SK_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; - private static final String QCP_N_QSCD_OID = "0.4.0.194112.1.2"; - - private QualifiedSignatureCertificatePurposeValidator validator; - - @BeforeEach - void setUp() { - validator = new QualifiedSignatureCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(QUALIFIED_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) - void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); - } - - @Test - void validate_QsStatementsExtensionIsMissing_throwException() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - X509Certificate cert = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); - } - - @Test - void validate_qsStatementsDoesNotHaveElectronicSigning_throwException() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), - new DERSequence()); - PolicyInformation qcpNQscdPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(QCP_N_QSCD_OID), - new DERSequence()); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); - - QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); - X509Certificate cert = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) - .withQcStatement(qcStatement) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); - assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); - } - - private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.qualified.ETSIQCObjectIdentifiers; +import org.bouncycastle.asn1.x509.qualified.QCStatement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +class QualifiedSignatureCertificatePurposeValidatorTest { + + private static final String QUALIFIED_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/cert-choice-cert-40504040001.pem.cert"); + private static final String SK_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; + private static final String QCP_N_QSCD_OID = "0.4.0.194112.1.2"; + + private QualifiedSignatureCertificatePurposeValidator validator; + + @BeforeEach + void setUp() { + validator = new QualifiedSignatureCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + assertDoesNotThrow(() -> validator.validate(CertificateUtil.toX509Certificate(QUALIFIED_SIGNING_CERTIFICATE.getBytes(StandardCharsets.UTF_8)))); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not contain required qualified certificate policy OIDs", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(InvalidKeyUsageArgumentProvider.class) + void validate_keyUsageNonRepudiationIsMissing_throwException(KeyUsage keyUsage) { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).withKeyUsage(keyUsage).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have Non-Repudiation set in 'KeyUsage' extension", ex.getMessage()); + } + + @Test + void validate_QsStatementsExtensionIsMissing_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have 'QCStatements' extension", ex.getMessage()); + } + + @Test + void validate_qsStatementsDoesNotHaveElectronicSigning_throwException() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_POLICY_OID), + new DERSequence()); + PolicyInformation qcpNQscdPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(QCP_N_QSCD_OID), + new DERSequence()); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, qcpNQscdPolicy); + + QCStatement qcStatement = new QCStatement(ETSIQCObjectIdentifiers.id_etsi_qct_eseal); + X509Certificate cert = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withKeyUsage(new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation)) + .withQcStatement(qcStatement) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> validator.validate(cert)); + assertEquals("Certificate does not have electronic signature OID (0.4.0.1862.1.6.1) in QCStatements extension.", ex.getMessage()); + } + + private static class InvalidKeyUsageArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(null, new KeyUsage(KeyUsage.digitalSignature)).map(Arguments::of); + } + } +} diff --git a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java index ef2c984a..79ddd848 100644 --- a/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/RpChallengeGeneratorTest.java @@ -1,69 +1,69 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class RpChallengeGeneratorTest { - - @Test - void generate_defaultValueUsed() { - RpChallenge challenge = RpChallengeGenerator.generate(); - - assertNotNull(challenge); - assertEquals(64, challenge.value().length); - } - - @ParameterizedTest - @ValueSource(ints = {32, 43, 59, 64}) - void generate_providedValuesAreInAllowedRange(int allowedValue) { - RpChallenge challenge = RpChallengeGenerator.generate(allowedValue); - - assertNotNull(challenge); - assertEquals(allowedValue, challenge.value().length); - } - - @Test - void generate_providedValueIsLessThanAllowed_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(31)); - assertEquals("Length must be between 32 and 64", ex.getMessage()); - } - - @Test - void generate_providedValueIsMoreThanAllowed_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(65)); - assertEquals("Length must be between 32 and 64", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class RpChallengeGeneratorTest { + + @Test + void generate_defaultValueUsed() { + RpChallenge challenge = RpChallengeGenerator.generate(); + + assertNotNull(challenge); + assertEquals(64, challenge.value().length); + } + + @ParameterizedTest + @ValueSource(ints = {32, 43, 59, 64}) + void generate_providedValuesAreInAllowedRange(int allowedValue) { + RpChallenge challenge = RpChallengeGenerator.generate(allowedValue); + + assertNotNull(challenge); + assertEquals(allowedValue, challenge.value().length); + } + + @Test + void generate_providedValueIsLessThanAllowed_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(31)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); + } + + @Test + void generate_providedValueIsMoreThanAllowed_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> RpChallengeGenerator.generate(65)); + assertEquals("Length must be between 32 and 64", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java index 9d367811..4eb6df4e 100644 --- a/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/SessionEndResultErrorArgumentsProvider.java @@ -1,65 +1,65 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; -import ee.sk.smartid.exception.permanent.ProtocolFailureException; -import ee.sk.smartid.exception.permanent.SmartIdServerException; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.useraccount.DocumentUnusableException; -import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; -import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; -import ee.sk.smartid.exception.useraction.SessionTimeoutException; -import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedException; -import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; - -public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("USER_REFUSED", UserRefusedException.class), - Arguments.of("TIMEOUT", SessionTimeoutException.class), - Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), - Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), - Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), - Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), - Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), - Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), - Arguments.of("SERVER_ERROR", SmartIdServerException.class), - Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class), - Arguments.of("ACCOUNT_UNUSABLE", UserAccountUnusableException.class) - ); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.permanent.ExpectedLinkedSessionException; +import ee.sk.smartid.exception.permanent.ProtocolFailureException; +import ee.sk.smartid.exception.permanent.SmartIdServerException; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.useraccount.DocumentUnusableException; +import ee.sk.smartid.exception.useraccount.RequiredInteractionNotSupportedByAppException; +import ee.sk.smartid.exception.useraccount.UserAccountUnusableException; +import ee.sk.smartid.exception.useraction.SessionTimeoutException; +import ee.sk.smartid.exception.useraction.UserRefusedCertChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedException; +import ee.sk.smartid.exception.useraction.UserSelectedWrongVerificationCodeException; + +public class SessionEndResultErrorArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("USER_REFUSED", UserRefusedException.class), + Arguments.of("TIMEOUT", SessionTimeoutException.class), + Arguments.of("DOCUMENT_UNUSABLE", DocumentUnusableException.class), + Arguments.of("WRONG_VC", UserSelectedWrongVerificationCodeException.class), + Arguments.of("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP", RequiredInteractionNotSupportedByAppException.class), + Arguments.of("USER_REFUSED_CERT_CHOICE", UserRefusedCertChoiceException.class), + Arguments.of("PROTOCOL_FAILURE", ProtocolFailureException.class), + Arguments.of("EXPECTED_LINKED_SESSION", ExpectedLinkedSessionException.class), + Arguments.of("SERVER_ERROR", SmartIdServerException.class), + Arguments.of("UNKNOWN_RESULT", UnprocessableSmartIdResponseException.class), + Arguments.of("ACCOUNT_UNUSABLE", UserAccountUnusableException.class) + ); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableDataTest.java b/src/test/java/ee/sk/smartid/SignableDataTest.java index 3b04a6d9..82d57a2d 100644 --- a/src/test/java/ee/sk/smartid/SignableDataTest.java +++ b/src/test/java/ee/sk/smartid/SignableDataTest.java @@ -1,68 +1,68 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Base64; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class SignableDataTest { - - private static final byte[] TEST_DATA = "Test data".getBytes(); - - @Test - void getDigestInBase64() { - SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); - assertEquals(Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512)), signableData.getDigestInBase64()); - assertEquals(HashAlgorithm.SHA_512, signableData.hashAlgorithm()); - } - - @Test - void calculateHash() { - SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); - assertArrayEquals(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512), signableData.calculateHash()); - } - - @ParameterizedTest - @NullAndEmptySource - void emptyHashProvided_throwException(byte[] dataToSign) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(dataToSign)); - assertEquals("Parameter 'dataToSign' cannot be empty", ex.getMessage()); - } - - @Test - void defaultHashAlgorithmSetToNull_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(TEST_DATA, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableDataTest { + + private static final byte[] TEST_DATA = "Test data".getBytes(); + + @Test + void getDigestInBase64() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertEquals(Base64.getEncoder().encodeToString(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512)), signableData.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableData.hashAlgorithm()); + } + + @Test + void calculateHash() { + SignableData signableData = new SignableData(TEST_DATA, HashAlgorithm.SHA_512); + assertArrayEquals(DigestCalculator.calculateDigest(TEST_DATA, HashAlgorithm.SHA_512), signableData.calculateHash()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashProvided_throwException(byte[] dataToSign) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(dataToSign)); + assertEquals("Parameter 'dataToSign' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmSetToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableData(TEST_DATA, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SignableHashTest.java b/src/test/java/ee/sk/smartid/SignableHashTest.java index e269662b..7d6bdf7c 100644 --- a/src/test/java/ee/sk/smartid/SignableHashTest.java +++ b/src/test/java/ee/sk/smartid/SignableHashTest.java @@ -1,64 +1,64 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Base64; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class SignableHashTest { - - private static final byte[] DIGEST = DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512); - - @Test - void getDigestInBase64() { - SignableHash signableHash = new SignableHash(DIGEST, HashAlgorithm.SHA_512); - - assertEquals(Base64.getEncoder().encodeToString(DIGEST), signableHash.getDigestInBase64()); - assertEquals(HashAlgorithm.SHA_512, signableHash.hashAlgorithm()); - } - - @ParameterizedTest - @NullAndEmptySource - void emptyHashValueProvided_throwException(byte[] hash) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(hash)); - assertEquals("Parameter 'hash' cannot be empty", ex.getMessage()); - } - - @Test - void defaultHashAlgorithmOverriddenToNull_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(DIGEST, null)); - assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Base64; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class SignableHashTest { + + private static final byte[] DIGEST = DigestCalculator.calculateDigest("Test data".getBytes(), HashAlgorithm.SHA_512); + + @Test + void getDigestInBase64() { + SignableHash signableHash = new SignableHash(DIGEST, HashAlgorithm.SHA_512); + + assertEquals(Base64.getEncoder().encodeToString(DIGEST), signableHash.getDigestInBase64()); + assertEquals(HashAlgorithm.SHA_512, signableHash.hashAlgorithm()); + } + + @ParameterizedTest + @NullAndEmptySource + void emptyHashValueProvided_throwException(byte[] hash) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(hash)); + assertEquals("Parameter 'hash' cannot be empty", ex.getMessage()); + } + + @Test + void defaultHashAlgorithmOverriddenToNull_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new SignableHash(DIGEST, null)); + assertEquals("Parameter 'hashAlgorithm' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java index bf077014..5f7d52d2 100644 --- a/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java +++ b/src/test/java/ee/sk/smartid/SignatureResponseValidatorTest.java @@ -1,587 +1,587 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; -import ee.sk.smartid.rest.dao.SessionCertificate; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionResult; -import ee.sk.smartid.rest.dao.SessionResultDetails; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; - -class SignatureResponseValidatorTest { - - private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); - - // TODO - 31.08.25: replace these values when the test accounts are available - private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); - private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; - - private SignatureResponseValidator signatureResponseValidator; - - @BeforeEach - void setUp() { - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() - .withOcspEnabled(false) - .build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - } - - @Test - void validate_validRawDigestSignature() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); - assertEquals("OK", response.getEndResult()); - } - - @ParameterizedTest - @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) - void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel); - assertEquals("OK", response.getEndResult()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @ParameterizedTest - @EnumSource(FlowType.class) - void validate_flowTypesAreSupported(FlowType flowType) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss", flowType); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); - assertEquals("OK", response.getEndResult()); - } - - @Test - void validate_nqSigning_ok() { - SessionStatus sessionStatus = toNqignatureSessionStatus(); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); - assertEquals("OK", response.getEndResult()); - } - - @Test - void validate_stateParameterMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setState(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'state' is empty", ex.getMessage()); - } - - @Test - void validate_sessionNotComplete() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setState("RUNNING"); - - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertTrue(ex.getMessage().contains("Session is not complete")); - } - - @Test - void validate_sessionResultNull() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result' is missing", ex.getMessage()); - } - - @Test - void validate_missingDocumentNumber() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getResult().setDocumentNumber(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); - } - - @Test - void validate_missingInteractionFlowUsed() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setInteractionTypeUsed(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); - } - - @Test - void validate_signatureProtocolMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); - } - - @Nested - class CertificateValidation { - - @Test - void validate_missingCertificate() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setCert(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert' is missing", ex.getMessage()); - } - - @Test - void validate_missingCertificateValue() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setValue(null); - - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); - } - - @Test - void validate_certificateLevelMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setCertificateLevel(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); - } - - @Test - void validate_certificateLevelMismatch() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getCert().setCertificateLevel("ADVANCED"); - - var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); - } - } - - @Nested - class SignatureValidation { - - @Test - void validate_rawDigestUnexpectedAlgorithm() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_unknownSignatureProtocol() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); - sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); - } - - @ParameterizedTest - @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) - void validate_handleSessionEndResultErrors(String endResult, Class expectedException) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult(endResult); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - } - - @ParameterizedTest - @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) - void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { - var sessionResultDetails = new SessionResultDetails(); - sessionResultDetails.setInteraction(interaction); - - var sessionResult = new SessionResult(); - sessionResult.setEndResult("USER_REFUSED_INTERACTION"); - sessionResult.setDetails(sessionResultDetails); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - - assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - } - - @Test - void validate_endResultMissing_throwsException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); - - sessionStatus.getResult().setEndResult(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); - } - - @Test - void validate_sessionStatusNull() { - var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, CertificateLevel.QUALIFIED)); - assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); - } - - @Test - void validate_signatureMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.setSignature(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); - } - - @Test - void validate_signatureValueMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setValue(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); - } - - @Test - void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setValue("invalid-not+encoded+value"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithm(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"SHA-1", "invalid"}) - void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_flowTypeMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setFlowType(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); - } - - @Test - void validate_invalidFlowType() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmNotSupported() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_signatureAlgorithmNotRsassaPss() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); - sessionStatus.getSignature().setSignatureAlgorithm("rsa"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); - } - - @Nested - class SignatureAlgorithmParametersValidations { - - @Test - void validate_signatureAlgorithmParametersMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().setSignatureAlgorithmParameters(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_hashAlgorithmMissing(String hashAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); - } - - @Test - void validate_invalidHashAlgorithm() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_maskGenAlgorithmIsMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); - } - - @Test - void validate_invalidMaskGenAlgorithmName() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .setParameters(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm(hashAlgorithm); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); - } - - @Test - void validate_maskGenHashAlgorithmInvalid() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() - .getParameters().setHashAlgorithm("INVALID-HASH"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); - } - - @Test - void validate_mismatchedHashAlgorithms() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); - } - - @Test - void validate_saltLengthIsMissing() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); - } - - @Test - void validate_invalidSaltLength() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); - } - - @Test - void validate_invalidTrailerField() { - SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); - sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); - assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); - } - } - } - - - private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, - String signatureAlgorithm) { - return toQualifiedSignatureSessionStatus(signatureProtocol, signatureAlgorithm, FlowType.QR); - } - - private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, - String signatureAlgorithm, - FlowType flowType) { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("QUALIFIED"); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); - - var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params, flowType); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(signatureProtocol); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - return sessionStatus; - } - - private static SessionStatus toNqignatureSessionStatus() { - var sessionResult = new SessionResult(); - sessionResult.setEndResult("OK"); - sessionResult.setDocumentNumber("PNOEE-12345678901"); - - var sessionCertificate = new SessionCertificate(); - sessionCertificate.setCertificateLevel("ADVANCED"); - sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); - - var params = toSessionSignatureAlgorithmParams(); - var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params, FlowType.QR); - - var sessionStatus = new SessionStatus(); - sessionStatus.setState("COMPLETE"); - sessionStatus.setResult(sessionResult); - sessionStatus.setCert(sessionCertificate); - sessionStatus.setSignature(sessionSignature); - sessionStatus.setSignatureProtocol(SignatureProtocol.RAW_DIGEST_SIGNATURE.name()); - sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); - - return sessionStatus; - } - - private static SessionSignature toSessionSignature(String signatureValue, - String signatureAlgorithm, - SessionSignatureAlgorithmParameters params, - FlowType flowType) { - var sessionSignature = new SessionSignature(); - sessionSignature.setValue(signatureValue); - sessionSignature.setSignatureAlgorithm(signatureAlgorithm); - sessionSignature.setSignatureAlgorithmParameters(params); - sessionSignature.setServerRandom("serverRandomValue"); - sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); - sessionSignature.setFlowType(flowType.getDescription()); - return sessionSignature; - } - - private static SessionSignatureAlgorithmParameters toSessionSignatureAlgorithmParams() { - var mgfParams = new SessionMaskGenAlgorithmParameters(); - mgfParams.setHashAlgorithm("SHA-512"); - - var mgf = new SessionMaskGenAlgorithm(); - mgf.setAlgorithm("id-mgf1"); - mgf.setParameters(mgfParams); - - var params = new SessionSignatureAlgorithmParameters(); - params.setHashAlgorithm("SHA-512"); - params.setMaskGenAlgorithm(mgf); - params.setSaltLength(64); - params.setTrailerField("0xbc"); - return params; - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.CertificateLevelMismatchException; +import ee.sk.smartid.rest.dao.SessionCertificate; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithm; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionResult; +import ee.sk.smartid.rest.dao.SessionResultDetails; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; + +class SignatureResponseValidatorTest { + + private static final String SIGN_CERT = FileUtil.readFileToString("test-certs/sign-cert-40504040001.pem.crt"); + + // TODO - 31.08.25: replace these values when the test accounts are available + private static final String NQ_SIGNING_CERTIFICATE = FileUtil.readFileToString("test-certs/nq-signing-cert.pem"); + private static final String NQ_SIGNATURE_VALUE = "NVGdK0YNpyKWEK5YhyrZt0rjtczzlsSi9tw2KS8iw13cZbiPwCr1/v35By7KkGtZ7fY+s9ebG9NbiIldnJ+wtqgjI4ZlDMRsoepgMsNPQD66kAPObUylv7NdZ41O0i/RB8DUYHcd5RHnYhqN9wPdd4iNtzfkMhqlJsZLT4cYOV1cNIfQSQnHOekA8Qbq1CASt2i7i8cIQ2v5+CfFwmSBdkZGrInVlbptLK4pKpX7kYjzQ9sq+1ua9A+6ZHBE/nCdw/Oa0jXsnM3E1KDDQzSO5qafkW4LzEpGvaRn4lRXPxPmgg0m7z5TEZa0VXhBPr9qvBI7SDQDov4OMUku6WyKdEb+4qC9lR+u+T2drpPe4W9vdKodzjL/kalMyHITW4bfl9szMSdz0EF6oDUjwkNyzaUdms8kODLOkWKHMQjLK7/s00VHbt9i0uHERdUwU78XsnTBjw6oM0R1/WVdPu7FOzF/nETOZiWmziycieFj4Y2hhaPn2S/PmGqXcNpWipXw2kdVNRL+Kn7ryiz4ojXp7U2+0ZUi2r6nyt/AR/hbowSwbCn8tKFssDTZacYSsjhdpcyD6tsy3yc7tQqSHXAgAIy3k6EFqvM0ehIO0HAGCsyY4iVUjDluz4Bd3jurERFtu6GnLwGpX8fPh/CgvQh8O1XwI23cwe/Ojn6i7J155TL107kczNv1pD8oppTAd7Oe8bZCI7YDqEhFGwMpEeiSb80V5Deg3LwCYlQtenl04vFol+9Vij22RJpVvssTi0fJ8Vxgzm3Xtoak/R0U9fHiFsGB/eVrM3h27twztYwU49ti/ZYs/7Ow+RZGq7Kbr6KXyxdh9j7Mva5x5NBr2x6kJFBbJKjj0o+FRZJX6YTraup975+Oxvp13WICAPTtdNvRCkVoXKFOFjG040b4TFsPdny+iY3PBx4wTef/b4GX22MlAjVtBgw4x+XRoPO9F6X5wYFlw2UPLY0vPltWOXarR/AyXqyxBigiS/Sho090pH7nD6YZ2s7bp9jnqtWnzqWb"; + + private SignatureResponseValidator signatureResponseValidator; + + @BeforeEach + void setUp() { + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder() + .withOcspEnabled(false) + .build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + } + + @Test + void validate_validRawDigestSignature() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + + @ParameterizedTest + @EnumSource(value = CertificateLevel.class, names = {"QUALIFIED", "QSCD"}) + void validate_returnedCertificateLevelSameAsRequested(CertificateLevel certificateLevel) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, certificateLevel); + assertEquals("OK", response.getEndResult()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @ParameterizedTest + @EnumSource(FlowType.class) + void validate_flowTypesAreSupported(FlowType flowType) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss", flowType); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED); + assertEquals("OK", response.getEndResult()); + } + + @Test + void validate_nqSigning_ok() { + SessionStatus sessionStatus = toNqignatureSessionStatus(); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + SignatureResponse response = signatureResponseValidator.validate(sessionStatus, CertificateLevel.ADVANCED); + assertEquals("OK", response.getEndResult()); + } + + @Test + void validate_stateParameterMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'state' is empty", ex.getMessage()); + } + + @Test + void validate_sessionNotComplete() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setState("RUNNING"); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertTrue(ex.getMessage().contains("Session is not complete")); + } + + @Test + void validate_sessionResultNull() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result' is missing", ex.getMessage()); + } + + @Test + void validate_missingDocumentNumber() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getResult().setDocumentNumber(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result.documentNumber' is empty", ex.getMessage()); + } + + @Test + void validate_missingInteractionFlowUsed() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setInteractionTypeUsed(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'interactionTypeUsed' is empty", ex.getMessage()); + } + + @Test + void validate_signatureProtocolMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signatureProtocol' is empty", ex.getMessage()); + } + + @Nested + class CertificateValidation { + + @Test + void validate_missingCertificate() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setCert(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert' is missing", ex.getMessage()); + } + + @Test + void validate_missingCertificateValue() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setValue(null); + + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert.value' is empty", ex.getMessage()); + } + + @Test + void validate_certificateLevelMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'cert.certificateLevel' is empty", ex.getMessage()); + } + + @Test + void validate_certificateLevelMismatch() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getCert().setCertificateLevel("ADVANCED"); + + var ex = assertThrows(CertificateLevelMismatchException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signer's certificate is below requested certificate level", ex.getMessage()); + } + } + + @Nested + class SignatureValidation { + + @Test + void validate_rawDigestUnexpectedAlgorithm() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unexpectedAlgorithm"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + sessionStatus.getSignature().setSignatureAlgorithm("unexpectedAlgorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_unknownSignatureProtocol() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("UNKNOWN_PROTOCOL", "rsassa-pss"); + sessionStatus.setSignatureProtocol("UNKNOWN_PROTOCOL"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signatureProtocol' has unsupported value", ex.getMessage()); + } + + @ParameterizedTest + @ArgumentsSource(SessionEndResultErrorArgumentsProvider.class) + void validate_handleSessionEndResultErrors(String endResult, Class expectedException) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult(endResult); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + } + + @ParameterizedTest + @ArgumentsSource(UserRefusedInteractionArgumentsProvider.class) + void validate_endResultIsUserRefusedInteraction(String interaction, Class expectedException) { + var sessionResultDetails = new SessionResultDetails(); + sessionResultDetails.setInteraction(interaction); + + var sessionResult = new SessionResult(); + sessionResult.setEndResult("USER_REFUSED_INTERACTION"); + sessionResult.setDetails(sessionResultDetails); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + + assertThrows(expectedException, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + } + + @Test + void validate_endResultMissing_throwsException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignatureProtocol("RAW_DIGEST_SIGNATURE"); + + sessionStatus.getResult().setEndResult(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'result.endResult' is empty", ex.getMessage()); + } + + @Test + void validate_sessionStatusNull() { + var ex = assertThrows(SmartIdClientException.class, () -> signatureResponseValidator.validate(null, CertificateLevel.QUALIFIED)); + assertEquals("Parameter 'sessionStatus' is not provided", ex.getMessage()); + } + + @Test + void validate_signatureMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.setSignature(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature' is missing", ex.getMessage()); + } + + @Test + void validate_signatureValueMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.value' is empty", ex.getMessage()); + } + + @Test + void validate_signatureValueIsNotInBase64EncodedFormat_throwException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setValue("invalid-not+encoded+value"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.value' does not have Base64-encoded value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' is missing", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHA-1", "invalid"}) + void validate_invalidSignatureAlgorithmIsProvided(String invalidSignatureAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithm(invalidSignatureAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_flowTypeMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field `signature.flowType` is empty", ex.getMessage()); + } + + @Test + void validate_invalidFlowType() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setFlowType("UNSUPPORTED_FLOW"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.flowType' has unsupported value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmNotSupported() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "unsupported-algorithm"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_signatureAlgorithmNotRsassaPss() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsa"); + sessionStatus.getSignature().setSignatureAlgorithm("rsa"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithm' has unsupported value", ex.getMessage()); + } + + @Nested + class SignatureAlgorithmParametersValidations { + + @Test + void validate_signatureAlgorithmParametersMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().setSignatureAlgorithmParameters(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmMissing(String hashAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm(hashAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void validate_invalidHashAlgorithm() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_maskGenAlgorithmIsMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setMaskGenAlgorithm(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_maskGenAlgorithmAlgorithmIsEmpty(String algorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm(algorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' is empty", ex.getMessage()); + } + + @Test + void validate_invalidMaskGenAlgorithmName() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().setAlgorithm("INVALID"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.algorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_maskGenHashAlgorithmParametersAreMissing_throwException() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .setParameters(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters' is missing", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_hashAlgorithmInMaskGenHashAlgorithmParametersIsEmpty(String hashAlgorithm) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm(hashAlgorithm); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' is empty", ex.getMessage()); + } + + @Test + void validate_maskGenHashAlgorithmInvalid() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm() + .getParameters().setHashAlgorithm("INVALID-HASH"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", ex.getMessage()); + } + + @Test + void validate_mismatchedHashAlgorithms() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().getMaskGenAlgorithm().getParameters().setHashAlgorithm("SHA-256"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", ex.getMessage()); + } + + @Test + void validate_saltLengthIsMissing() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(null); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' is missing", ex.getMessage()); + } + + @Test + void validate_invalidSaltLength() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setSaltLength(32); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_signatureAlgorithmParametersTrailerFieldEmptyOrNull(String trailerField) { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField(trailerField); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` is empty", ex.getMessage()); + } + + @Test + void validate_invalidTrailerField() { + SessionStatus sessionStatus = toQualifiedSignatureSessionStatus("RAW_DIGEST_SIGNATURE", "rsassa-pss"); + sessionStatus.getSignature().getSignatureAlgorithmParameters().setTrailerField("0xab"); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> signatureResponseValidator.validate(sessionStatus, CertificateLevel.QUALIFIED)); + assertEquals("Signature status field `signature.signatureAlgorithmParameters.trailerField` has unsupported value", ex.getMessage()); + } + } + } + + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm) { + return toQualifiedSignatureSessionStatus(signatureProtocol, signatureAlgorithm, FlowType.QR); + } + + private static SessionStatus toQualifiedSignatureSessionStatus(String signatureProtocol, + String signatureAlgorithm, + FlowType flowType) { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("QUALIFIED"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(SIGN_CERT)); + + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature("expectedDigest", signatureAlgorithm, params, flowType); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(signatureProtocol); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static SessionStatus toNqignatureSessionStatus() { + var sessionResult = new SessionResult(); + sessionResult.setEndResult("OK"); + sessionResult.setDocumentNumber("PNOEE-12345678901"); + + var sessionCertificate = new SessionCertificate(); + sessionCertificate.setCertificateLevel("ADVANCED"); + sessionCertificate.setValue(CertificateUtil.getEncodedCertificateData(NQ_SIGNING_CERTIFICATE)); + + var params = toSessionSignatureAlgorithmParams(); + var sessionSignature = toSessionSignature(NQ_SIGNATURE_VALUE, SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), params, FlowType.QR); + + var sessionStatus = new SessionStatus(); + sessionStatus.setState("COMPLETE"); + sessionStatus.setResult(sessionResult); + sessionStatus.setCert(sessionCertificate); + sessionStatus.setSignature(sessionSignature); + sessionStatus.setSignatureProtocol(SignatureProtocol.RAW_DIGEST_SIGNATURE.name()); + sessionStatus.setInteractionTypeUsed("displayTextAndPIN"); + + return sessionStatus; + } + + private static SessionSignature toSessionSignature(String signatureValue, + String signatureAlgorithm, + SessionSignatureAlgorithmParameters params, + FlowType flowType) { + var sessionSignature = new SessionSignature(); + sessionSignature.setValue(signatureValue); + sessionSignature.setSignatureAlgorithm(signatureAlgorithm); + sessionSignature.setSignatureAlgorithmParameters(params); + sessionSignature.setServerRandom("serverRandomValue"); + sessionSignature.setUserChallenge("QWxwaGFFenItMTIzNDU2Nzg5MDEyMzQ1Njc4OTAx"); + sessionSignature.setFlowType(flowType.getDescription()); + return sessionSignature; + } + + private static SessionSignatureAlgorithmParameters toSessionSignatureAlgorithmParams() { + var mgfParams = new SessionMaskGenAlgorithmParameters(); + mgfParams.setHashAlgorithm("SHA-512"); + + var mgf = new SessionMaskGenAlgorithm(); + mgf.setAlgorithm("id-mgf1"); + mgf.setParameters(mgfParams); + + var params = new SessionSignatureAlgorithmParameters(); + params.setHashAlgorithm("SHA-512"); + params.setMaskGenAlgorithm(mgf); + params.setSaltLength(64); + params.setTrailerField("0xbc"); + return params; + } +} diff --git a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java index 5dfff34e..4ab8d7ac 100644 --- a/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java +++ b/src/test/java/ee/sk/smartid/SignatureValueValidatorImplTest.java @@ -1,121 +1,121 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Base64; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class SignatureValueValidatorImplTest { - - // TODO - 22.08.25: replace these values when test accounts are available - private static final String CERT = "MIIHSjCCBtCgAwIBAgIQBQHi3vqqZg+tDaGzQeB2GzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUwNzI5MDgxMTAzWhcNMjgwNzI4MDgxMTAyWjBfMQswCQYDVQQGEwJFRTEUMBIGA1UEAwwLTVVTRVIsVVJNQVMxDjAMBgNVBAQMBU1VU0VSMQ4wDAYDVQQqDAVVUk1BUzEaMBgGA1UEBRMRUE5PRUUtMzkwMDMwMTI3OTgwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCf8qQkO51SM/Gdw63LObpk4kwutMSqW345PU4HC+HqQ2H03fTludjY7iBCgEWmXQjoTt6vQgDGPfBlydjZiu2GUSCL/f2DTv76BuWzR/Jw6q4+R86GRhlMJFqfqE2gqCIddVbUx+qYZ37qCddqgIoRYejdrUeWopp2xzya5gt41FM9By95e3pS/1tug7aAlPoT3Tg18+13qqru1SDGxYW+0NVojesYX3Pzz8Exz2dWcFWwMqoU3SMlAULHDC9OPMtuZBSZA2tvyuD+CHHsU13LI46iDRU2j9BVr9EBuO/uvL3U5eIkX0gpy5bdo/TWmXDijTb5udXO9cz+GMaCQTx4yuBTnC31pHw/qrEp00FRZy7yiG0expv7w4c0YiziMFK8GfhnPmNAVEyjTWImmckK9SiIZH0F/oU1VZvtX3aXsmoTzEwpzAy3KPiKxJ0ZSSsVHV+G1nZvx/1mRxKcT+rOzNcx7iY9uAzin9ajPLYTukWsGVOTgQ2GxpYrEhuf8PvQlZ62BVIvfS5swhlwXzMU8oEAsHCpUVDNCLtckkKBgoy9pYZyKbXUtUP1TTEL3ZC9/4h3Udmao6JNWp5niyHDWVpF6r56O/ORZGx1GlT1P+G9rK6bBteptvNWillGPMA5E1fdwSci7/eH8amSED0CAy0rlq+0CdMdnpasqyG5oDmYJncWhhEozQ2fI7SkvNgSiMxDnJXhi8/Zvh4j+29eC7fqG5ZsLxQ1YqaK8XsIsNJ2Lxj0BhrEgU7Zz5lILUdOILEfU1S2Wi4Ow1P23dAP/O+o6u4SDSKSM2+C5s9daq/5zJ2w2s/B8JB8Mat5MPJuzKrvSnYMIUzQjtlsuMBRIRbHmHtCjDXufF11BOCLfPUYU5GDvk6MY51+p/hZrAowQHWZYI+271UxJR9I1dCTNvo1HsiNEnLSgdOikWvmykqiDVWPe6SiRpVKBQ7MkhgvF/CrHGG0S4GBuG6E2OHEMKl73CWuqU8MrPSOQvaXY7f99ZGK9RL1OG8oxRJpJNECAwEAAaOCAo8wggKLMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTM5MDAzMDEyNzk4LUZGTDgtUTB5BgNVHSAEcjBwMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCQYHBACL7EABAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5OTAwMzAxMTIwMDAwWjCBrgYIKwYBBQUHAQMEgaEwgZ4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwCAYGBACORgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJlbjA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUq5xLZIjeh1p1kreds8ie7OgpfmwwDgYDVR0PAQH/BAQDAgZAMAoGCCqGSM49BAMDA2gAMGUCMQCdrnNqlxbO/N6FELvGd4MHeNjTIpdDSj+6Htu6W7KRFleQGe8zhK9yA2l/zSerZvwCMGgbT0nvtgyoXBhSsUhY3RWTMiee4nKn7aBKqcmrDuHC9I9o67WpttfSE4srvL+qWQ=="; - private static final byte[] PAYLOAD = Base64.getDecoder().decode(("PGRzOlNpZ25lZEluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q" + "+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGE1MTIiPjwvZHM6U2lnbmF0dXJlTWV0aG9kPjxkczpSZWZlcmVuY2UgSWQ9InItaWQtNzcwMDA4OTNlNWU1YmVjOGMwY2IyOThjNmFkMGY0YTQtMSIgVVJJPSJkdW1teS5wZGYiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5QZmVkTkt1OHFaTUk1NXk1UkdIQmlUV0NZRTFvTXBwQi9VdnNHSVhtcmJRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PGRzOlJlZmVyZW5jZSBUeXBlPSJodHRwOi8vdXJpLmV0c2kub3JnLzAxOTAzI1NpZ25lZFByb3BlcnRpZXMiIFVSST0iI3hhZGVzLWlkLTc3MDAwODkzZTVlNWJlYzhjMGNiMjk4YzZhZDBmNGE0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPjFZd014blRUYmwwZXB5S0g0OEZ0WXFDb3pNbzAxem03NWpwV1pWNDJJNlk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+").getBytes(StandardCharsets.UTF_8)); - private static final byte[] SIGNATURE_VALUE = Base64.getDecoder().decode("UEVKOrz3Mr+qAXyOjGEt3Nnb8andzicBcEdbb4T2qVyGUslHdeJfgkXccPqBnjmEbL7xoU7eHVkO02K+XNseVY5UBHnXDlMBj1TyCnfelfiZFpAppgWmHKBXC11yE1OhtQ5/siaokPGBX1nZM2rzGlHYWxXYZrOGHCrm7gQEbClL342N6bEzeVVVPnxnxDEkzpNMFvY8UIL3C55WPPNKLBzFwSfduNcLaBiHc4ghaIiJebQc1h+Kad5OAYeu35v70k4HVePbDDp1Cb7RXfMRyUx7GNFSTZiKrG16XO8krp+d9T10SGRbZNoTzxvXBjtb8SjXM6Zvx0tiNdVnsWBrEylGzjS2DVU2+MDbek9QxlxqUU5E5H/WrelywgGTEzfZekowjofQjkYXaEAvNTK8x8Me1wIJThZwfrOy6H8MyPAdgAwl7fDwsZG6QhRCeG+9VY4CtmcII6YMZccCFCy9X3SJvXga4OcSrPi+Nwh3tfvJ5pkYvLliVKSCDpslTZk7JQYcQsJ1DVefMW6BfA+V3iX35mG/VHPo789BpzlZL6Ebs/dxNSEnyyWTDECFl2k2/B38w9jO4OuFLLg/U0AvM6ZLNlLWUjsKKg1s4U+SGlLc7r3hxaWCCwx4/NP2h8f3MTquxOCt+7WrjvCNOQ33bKcFGjYyCWpfGAfVgfMenp4oa40A1+Or+Px4Sd5yD3ZTnPSMYh2UzFZOiejGAS/koBYhn60P5PKRpEkC0nq+WQJD58soelH1EKifLoBtYNzhNOAuOfGRI5nEsW94TZ8hbC/mIEBmMnhKr9Lq/+glxbqskwOavWIF5xeWTKeSt2ErvgtNxX3hTlGxdNavwPi+/qtLikrNoirE26t1WFyPMaeH6hm0rIW42h5c0IvsXrQ4258uJzpZPe/RLbjdy62wi1S35PmowFUFImlHDKSIj4plEVXkrApZDV+/bL0VR6PNr7bsIZqgamL9OyLm6vTunP+A7Q+zpaZxuun17SC1QsthiGGBk03uf4CpNVVUpsO3".getBytes(StandardCharsets.UTF_8)); - - private SignatureValueValidator signatureValueValidator; - - @BeforeEach - void setUp() { - signatureValueValidator = new SignatureValueValidatorImpl(); - } - - @Test - void validate() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(CERT); - RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); - - assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); - } - - @ParameterizedTest - @ArgumentsSource(EmptyInputArgumentProvider.class) - void validate_InputParametersNotProvided_throwException(byte[] signatureValue, byte[] payload, X509Certificate certificate, RsaSsaPssParameters rsaSsaPssParameters) { - assertThrows(SmartIdClientException.class, () -> signatureValueValidator.validate(signatureValue, payload, certificate, rsaSsaPssParameters)); - } - - @Test - void validateSignatureValue_IsInvalid_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> signatureValueValidator.validate( - "invalidValue".getBytes(StandardCharsets.UTF_8), - PAYLOAD, - CertificateUtil.toX509CertificateFromEncodedString(CERT), - toRsaSsaPssParameters())); - assertEquals("Signature value validation failed", ex.getMessage()); - } - - @Test - void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, - () -> signatureValueValidator.validate( - SIGNATURE_VALUE, - "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), - CertificateUtil.toX509CertificateFromEncodedString(CERT), - toRsaSsaPssParameters())); - assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); - } - - private static RsaSsaPssParameters toRsaSsaPssParameters() { - RsaSsaPssParameters rsaSsaPssParameters = new RsaSsaPssParameters(); - rsaSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.SHA_512); - rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); - rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); - rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); - rsaSsaPssParameters.setTrailerField(TrailerField.BC); - return rsaSsaPssParameters; - } - - private static class EmptyInputArgumentProvider implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) throws CertificateException { - return Stream.of( - Arguments.of(null, null, null, null), - Arguments.of(new byte[0], null, null, null), - Arguments.of(new byte[0], new byte[0], null, null), - Arguments.of(new byte[0], new byte[0], CertificateUtil.toX509CertificateFromEncodedString(CERT), null) - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class SignatureValueValidatorImplTest { + + // TODO - 22.08.25: replace these values when test accounts are available + private static final String CERT = "MIIHSjCCBtCgAwIBAgIQBQHi3vqqZg+tDaGzQeB2GzAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjUwNzI5MDgxMTAzWhcNMjgwNzI4MDgxMTAyWjBfMQswCQYDVQQGEwJFRTEUMBIGA1UEAwwLTVVTRVIsVVJNQVMxDjAMBgNVBAQMBU1VU0VSMQ4wDAYDVQQqDAVVUk1BUzEaMBgGA1UEBRMRUE5PRUUtMzkwMDMwMTI3OTgwggMiMA0GCSqGSIb3DQEBAQUAA4IDDwAwggMKAoIDAQCf8qQkO51SM/Gdw63LObpk4kwutMSqW345PU4HC+HqQ2H03fTludjY7iBCgEWmXQjoTt6vQgDGPfBlydjZiu2GUSCL/f2DTv76BuWzR/Jw6q4+R86GRhlMJFqfqE2gqCIddVbUx+qYZ37qCddqgIoRYejdrUeWopp2xzya5gt41FM9By95e3pS/1tug7aAlPoT3Tg18+13qqru1SDGxYW+0NVojesYX3Pzz8Exz2dWcFWwMqoU3SMlAULHDC9OPMtuZBSZA2tvyuD+CHHsU13LI46iDRU2j9BVr9EBuO/uvL3U5eIkX0gpy5bdo/TWmXDijTb5udXO9cz+GMaCQTx4yuBTnC31pHw/qrEp00FRZy7yiG0expv7w4c0YiziMFK8GfhnPmNAVEyjTWImmckK9SiIZH0F/oU1VZvtX3aXsmoTzEwpzAy3KPiKxJ0ZSSsVHV+G1nZvx/1mRxKcT+rOzNcx7iY9uAzin9ajPLYTukWsGVOTgQ2GxpYrEhuf8PvQlZ62BVIvfS5swhlwXzMU8oEAsHCpUVDNCLtckkKBgoy9pYZyKbXUtUP1TTEL3ZC9/4h3Udmao6JNWp5niyHDWVpF6r56O/ORZGx1GlT1P+G9rK6bBteptvNWillGPMA5E1fdwSci7/eH8amSED0CAy0rlq+0CdMdnpasqyG5oDmYJncWhhEozQ2fI7SkvNgSiMxDnJXhi8/Zvh4j+29eC7fqG5ZsLxQ1YqaK8XsIsNJ2Lxj0BhrEgU7Zz5lILUdOILEfU1S2Wi4Ow1P23dAP/O+o6u4SDSKSM2+C5s9daq/5zJ2w2s/B8JB8Mat5MPJuzKrvSnYMIUzQjtlsuMBRIRbHmHtCjDXufF11BOCLfPUYU5GDvk6MY51+p/hZrAowQHWZYI+271UxJR9I1dCTNvo1HsiNEnLSgdOikWvmykqiDVWPe6SiRpVKBQ7MkhgvF/CrHGG0S4GBuG6E2OHEMKl73CWuqU8MrPSOQvaXY7f99ZGK9RL1OG8oxRJpJNECAwEAAaOCAo8wggKLMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTM5MDAzMDEyNzk4LUZGTDgtUTB5BgNVHSAEcjBwMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCQYHBACL7EABAjAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5OTAwMzAxMTIwMDAwWjCBrgYIKwYBBQUHAQMEgaEwgZ4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwCAYGBACORgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFwGBgQAjkYBBTBSMFAWSmh0dHBzOi8vd3d3LnNraWRzb2x1dGlvbnMuZXUvcmVzb3VyY2VzL2NvbmRpdGlvbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJlbjA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIwMjRlLmNybDAdBgNVHQ4EFgQUq5xLZIjeh1p1kreds8ie7OgpfmwwDgYDVR0PAQH/BAQDAgZAMAoGCCqGSM49BAMDA2gAMGUCMQCdrnNqlxbO/N6FELvGd4MHeNjTIpdDSj+6Htu6W7KRFleQGe8zhK9yA2l/zSerZvwCMGgbT0nvtgyoXBhSsUhY3RWTMiee4nKn7aBKqcmrDuHC9I9o67WpttfSE4srvL+qWQ=="; + private static final byte[] PAYLOAD = Base64.getDecoder().decode(("PGRzOlNpZ25lZEluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q" + "+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGE1MTIiPjwvZHM6U2lnbmF0dXJlTWV0aG9kPjxkczpSZWZlcmVuY2UgSWQ9InItaWQtNzcwMDA4OTNlNWU1YmVjOGMwY2IyOThjNmFkMGY0YTQtMSIgVVJJPSJkdW1teS5wZGYiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiPjwvZHM6RGlnZXN0TWV0aG9kPjxkczpEaWdlc3RWYWx1ZT5QZmVkTkt1OHFaTUk1NXk1UkdIQmlUV0NZRTFvTXBwQi9VdnNHSVhtcmJRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PGRzOlJlZmVyZW5jZSBUeXBlPSJodHRwOi8vdXJpLmV0c2kub3JnLzAxOTAzI1NpZ25lZFByb3BlcnRpZXMiIFVSST0iI3hhZGVzLWlkLTc3MDAwODkzZTVlNWJlYzhjMGNiMjk4YzZhZDBmNGE0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPjFZd014blRUYmwwZXB5S0g0OEZ0WXFDb3pNbzAxem03NWpwV1pWNDJJNlk9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+").getBytes(StandardCharsets.UTF_8)); + private static final byte[] SIGNATURE_VALUE = Base64.getDecoder().decode("UEVKOrz3Mr+qAXyOjGEt3Nnb8andzicBcEdbb4T2qVyGUslHdeJfgkXccPqBnjmEbL7xoU7eHVkO02K+XNseVY5UBHnXDlMBj1TyCnfelfiZFpAppgWmHKBXC11yE1OhtQ5/siaokPGBX1nZM2rzGlHYWxXYZrOGHCrm7gQEbClL342N6bEzeVVVPnxnxDEkzpNMFvY8UIL3C55WPPNKLBzFwSfduNcLaBiHc4ghaIiJebQc1h+Kad5OAYeu35v70k4HVePbDDp1Cb7RXfMRyUx7GNFSTZiKrG16XO8krp+d9T10SGRbZNoTzxvXBjtb8SjXM6Zvx0tiNdVnsWBrEylGzjS2DVU2+MDbek9QxlxqUU5E5H/WrelywgGTEzfZekowjofQjkYXaEAvNTK8x8Me1wIJThZwfrOy6H8MyPAdgAwl7fDwsZG6QhRCeG+9VY4CtmcII6YMZccCFCy9X3SJvXga4OcSrPi+Nwh3tfvJ5pkYvLliVKSCDpslTZk7JQYcQsJ1DVefMW6BfA+V3iX35mG/VHPo789BpzlZL6Ebs/dxNSEnyyWTDECFl2k2/B38w9jO4OuFLLg/U0AvM6ZLNlLWUjsKKg1s4U+SGlLc7r3hxaWCCwx4/NP2h8f3MTquxOCt+7WrjvCNOQ33bKcFGjYyCWpfGAfVgfMenp4oa40A1+Or+Px4Sd5yD3ZTnPSMYh2UzFZOiejGAS/koBYhn60P5PKRpEkC0nq+WQJD58soelH1EKifLoBtYNzhNOAuOfGRI5nEsW94TZ8hbC/mIEBmMnhKr9Lq/+glxbqskwOavWIF5xeWTKeSt2ErvgtNxX3hTlGxdNavwPi+/qtLikrNoirE26t1WFyPMaeH6hm0rIW42h5c0IvsXrQ4258uJzpZPe/RLbjdy62wi1S35PmowFUFImlHDKSIj4plEVXkrApZDV+/bL0VR6PNr7bsIZqgamL9OyLm6vTunP+A7Q+zpaZxuun17SC1QsthiGGBk03uf4CpNVVUpsO3".getBytes(StandardCharsets.UTF_8)); + + private SignatureValueValidator signatureValueValidator; + + @BeforeEach + void setUp() { + signatureValueValidator = new SignatureValueValidatorImpl(); + } + + @Test + void validate() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(CERT); + RsaSsaPssParameters rsaSsaPssParameters = toRsaSsaPssParameters(); + + assertDoesNotThrow(() -> signatureValueValidator.validate(SIGNATURE_VALUE, PAYLOAD, certificate, rsaSsaPssParameters)); + } + + @ParameterizedTest + @ArgumentsSource(EmptyInputArgumentProvider.class) + void validate_InputParametersNotProvided_throwException(byte[] signatureValue, byte[] payload, X509Certificate certificate, RsaSsaPssParameters rsaSsaPssParameters) { + assertThrows(SmartIdClientException.class, () -> signatureValueValidator.validate(signatureValue, payload, certificate, rsaSsaPssParameters)); + } + + @Test + void validateSignatureValue_IsInvalid_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + "invalidValue".getBytes(StandardCharsets.UTF_8), + PAYLOAD, + CertificateUtil.toX509CertificateFromEncodedString(CERT), + toRsaSsaPssParameters())); + assertEquals("Signature value validation failed", ex.getMessage()); + } + + @Test + void validateSignatureValue_constructedPayloadDoesNotMatchTheSignature_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, + () -> signatureValueValidator.validate( + SIGNATURE_VALUE, + "payloadThatDoesNotMatch".getBytes(StandardCharsets.UTF_8), + CertificateUtil.toX509CertificateFromEncodedString(CERT), + toRsaSsaPssParameters())); + assertEquals("Provided signature value does not match the calculated signature value", ex.getMessage()); + } + + private static RsaSsaPssParameters toRsaSsaPssParameters() { + RsaSsaPssParameters rsaSsaPssParameters = new RsaSsaPssParameters(); + rsaSsaPssParameters.setDigestHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setMaskGenAlgorithm(MaskGenAlgorithm.ID_MGF1); + rsaSsaPssParameters.setMaskHashAlgorithm(HashAlgorithm.SHA_512); + rsaSsaPssParameters.setSaltLength(HashAlgorithm.SHA_512.getOctetLength()); + rsaSsaPssParameters.setTrailerField(TrailerField.BC); + return rsaSsaPssParameters; + } + + private static class EmptyInputArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) throws CertificateException { + return Stream.of( + Arguments.of(null, null, null, null), + Arguments.of(new byte[0], null, null, null), + Arguments.of(new byte[0], new byte[0], null, null), + Arguments.of(new byte[0], new byte[0], CertificateUtil.toX509CertificateFromEncodedString(CERT), null) + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdClientTest.java b/src/test/java/ee/sk/smartid/SmartIdClientTest.java index 04b5c8de..a44def42 100644 --- a/src/test/java/ee/sk/smartid/SmartIdClientTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdClientTest.java @@ -1,811 +1,811 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.time.Duration; -import java.time.Instant; -import java.util.List; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.VerificationCode; - -class SmartIdClientTest { - - private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - private static final String PERSON_CODE = "PNOEE-1234567890"; - private static final String INITIAL_CALLBACK_URL = "https://example.com/callback"; - - private SmartIdClient smartIdClient; - - @BeforeEach - void setUp() { - smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); - smartIdClient.setRelyingPartyName("DEMO"); - smartIdClient.setHostUrl("http://localhost:18089"); - smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkCertificateChoiceSession { - - @Test - void createSameDeviceCertificateChoiceSession() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createSameDeviceCertificateChoiceSessionWithAllFields() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .withNonce("d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk") - .withShareMdClientIpAddress(true) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createQrCodeCertificateChoiceSession() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationCertificateChoiceSession { - - @Test - void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - - NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - } - - @Test - void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - - NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withShareMdClientIpAddress(true) - .initCertificateChoice(); - - assertNotNull(response.sessionID()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkAuthenticationSession { - - @Test - void createDeviceLinkAuthentication_anonymous() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withDocumentNumber(DOCUMENT_NUMBER) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkSignatureSession { - - @Test - void createDeviceLinkSignature_withDocumentNumberSameDevice() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withDocumentNumberQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - - @Test - void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertNotNull(response.sessionID()); - assertNotNull(response.sessionToken()); - assertNotNull(response.sessionSecret()); - assertNotNull(response.deviceLinkBase()); - assertNotNull(response.receivedAt()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class CertificateByDocumentNumberRequest { - - @Test - void createCertificateRequest_withDocumentNumber() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", - "requests/sign/certificate-by-document-number-request-all-fields.json", - "responses/certificate-by-document-number-response.json"); - - CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.ADVANCED) - .getCertificateByDocumentNumber(); - - assertNotNull(response); - assertEquals(CertificateLevel.QUALIFIED, response.certificateLevel()); - assertNotNull(response.certificate()); - } - - @Test - void getCertificateByDocumentNumber_withUnknownState_throwsException() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", - "requests/sign/certificate-by-document-number-request-all-fields.json", - "responses/certificate-by-document-number-response-unknown-state.json"); - - CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.ADVANCED); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); - assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationAuthenticationSession { - - @Test - void createNotificationAuthentication_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - } - - @Test - void createNotificationAuthentication_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() - .withDocumentNumber(DOCUMENT_NUMBER) - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) - .initAuthenticationSession(); - - assertNotNull(response.sessionID()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class NotificationBasedSignatureSession { - - @Test - void createNotificationSignature_withDocumentNumber() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(64).getBytes()); - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertSessionResponse(response); - } - - @Test - void createNotificationSignature_withSemanticsIdentifier() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(64).getBytes()); - NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() - .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) - .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) - .withSignableHash(signableHash) - .initSignatureSession(); - - assertSessionResponse(response); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse response) { - assertNotNull(response.sessionID()); - VerificationCode verificationCode = response.vc(); - assertNotNull(verificationCode); - assertNotNull(verificationCode.type()); - assertNotNull(verificationCode.value()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class LinkedNotificationBasedSignatureSession { - - private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; - - @Test - void createLinkedNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, - "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", - "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withLinkedSessionID("10000000-0000-000-000-000000000000") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) - .initSignatureSession(); - - assertNotNull(response); - } - - @Test - void createLinkedNotificationSignature_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, - "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", - "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(new SignableData("Test data".getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withLinkedSessionID("10000000-0000-000-000-000000000000") - .withNonce("cmFuZG9tTm9uY2U=") - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) - .withShareMdClientIpAddress(true) - .initSignatureSession(); - - assertNotNull(response); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class SessionsStatus { - - @Test - void fetchFinalSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-successful-authentication.json"); - - SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); - - assertEquals("COMPLETE", status.getState()); - assertEquals("OK", status.getResult().getEndResult()); - } - - @Test - void getSessionStatus() { - SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-running.json"); - - SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); - - assertEquals("RUNNING", status.getState()); - assertNull(status.getResult()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForAuth { - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); - - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInitialCallbackUrl(request.initialCallbackUrl()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.AUTHENTICATION, deviceLinkType, response.sessionToken()); - } - - @Test - void createDynamicContent_authenticationWithQRCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_authenticationWithQRCodeImage() { - SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() - .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) - .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) - .withHashAlgorithm(HashAlgorithm.SHA3_512); - DeviceLinkSessionResponse response = builder.initAuthenticationSession(); - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForSignature { - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-same-device.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.SIGNATURE, deviceLinkType, response.sessionToken()); - } - - @Test - void createDynamicContent_withQrCode() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withElapsedSeconds(elapsed.getSeconds()) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_withQrCodeImage() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", - "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", - "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - var signableHash = new SignableHash("a".repeat(32).getBytes()); - - DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() - .withDocumentNumber(DOCUMENT_NUMBER) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) - .withSignableHash(signableHash); - DeviceLinkSessionResponse response = builder.initSignatureSession(); - DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); - - Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); - URI qrCodeUri = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withElapsedSeconds(elapsed.getSeconds()) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withDigest(signableHash.getDigestInBase64()) - .withInteractions(request.interactions()) - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DynamicContentForCertificateChoice { - - @Test - void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLink, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @Test - void createDynamicContent_createQrCodeImage() { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withNonce(Base64.toBase64String("randomNonce".getBytes())) - .withCertificateLevel(CertificateLevel.ADVANCED) - .initCertificateChoice(); - - long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); - - URI qrCodeUri = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withElapsedSeconds(elapsedSeconds) - .withLang("eng") - .buildDeviceLink(response.sessionSecret()); - - String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); - String[] qrCodeDataUriParts = qrCodeDataUri.split(","); - URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); - - assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); - } - - @ParameterizedTest - @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) - void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { - SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", - "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", - "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withInitialCallbackUrl(INITIAL_CALLBACK_URL) - .initCertificateChoice(); - - URI deviceLinkUri = smartIdClient.createDynamicContent() - .withDeviceLinkBase(response.deviceLinkBase().toString()) - .withDeviceLinkType(deviceLinkType) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(response.sessionToken()) - .withLang("eng") - .withInitialCallbackUrl("https://smart-id.com/callback") - .buildDeviceLink(response.sessionSecret()); - - assertUri(deviceLinkUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.sessionToken()); - } - } - - private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { - assertEquals("https", qrCodeUri.getScheme()); - assertEquals("smart-id.com", qrCodeUri.getHost()); - assertEquals("/device-link/", qrCodeUri.getPath()); - - assertTrue(qrCodeUri.getQuery().contains("version=1.0")); - assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); - assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); - assertTrue(qrCodeUri.getQuery().contains("lang=eng")); - assertTrue(qrCodeUri.getQuery().contains("authCode=")); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.VerificationCode; + +class SmartIdClientTest { + + private static final String DEMO_HOST_SSL_CERTIFICATE = FileUtil.readFileToString("sid_demo_sk_ee.pem"); + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + private static final String PERSON_CODE = "PNOEE-1234567890"; + private static final String INITIAL_CALLBACK_URL = "https://example.com/callback"; + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("http://localhost:18089"); + smartIdClient.setTrustedCertificates(DEMO_HOST_SSL_CERTIFICATE); + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkCertificateChoiceSession { + + @Test + void createSameDeviceCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createSameDeviceCertificateChoiceSessionWithAllFields() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .withNonce("d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk") + .withShareMdClientIpAddress(true) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createQrCodeCertificateChoiceSession() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationCertificateChoiceSession { + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifierAndOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + } + + @Test + void createNotificationCertificateChoice_withSemanticsIdentifierAndAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate-choice/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + + NotificationCertificateChoiceSessionResponse response = smartIdClient.createNotificationCertificateChoice() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withShareMdClientIpAddress(true) + .initCertificateChoice(); + + assertNotNull(response.sessionID()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkAuthenticationSession { + + @Test + void createDeviceLinkAuthentication_anonymous() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withDocumentNumber(DOCUMENT_NUMBER) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/authentication/device-link/etsi/PNOEE-1234567890", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkSignatureSession { + + @Test + void createDeviceLinkSignature_withDocumentNumberSameDevice() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withDocumentNumberQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes(), HashAlgorithm.SHA_512); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierSameDevice() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + + @Test + void createDeviceLinkSignature_withSemanticsIdentifierQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/etsi/PNOEE-1234567890", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertNotNull(response.sessionID()); + assertNotNull(response.sessionToken()); + assertNotNull(response.sessionSecret()); + assertNotNull(response.deviceLinkBase()); + assertNotNull(response.receivedAt()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class CertificateByDocumentNumberRequest { + + @Test + void createCertificateRequest_withDocumentNumber() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response.json"); + + CertificateByDocumentNumberResult response = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.ADVANCED) + .getCertificateByDocumentNumber(); + + assertNotNull(response); + assertEquals(CertificateLevel.QUALIFIED, response.certificateLevel()); + assertNotNull(response.certificate()); + } + + @Test + void getCertificateByDocumentNumber_withUnknownState_throwsException() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate/PNOEE-1234567890-MOCK-Q", + "requests/sign/certificate-by-document-number-request-all-fields.json", + "responses/certificate-by-document-number-response-unknown-state.json"); + + CertificateByDocumentNumberRequestBuilder builder = smartIdClient.createCertificateByDocumentNumber() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.ADVANCED); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, builder::getCertificateByDocumentNumber); + assertEquals("Queried certificate response field 'state' has unsupported value", ex.getMessage()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationAuthenticationSession { + + @Test + void createNotificationAuthentication_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/etsi/PNOEE-1234567890", + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + } + + @Test + void createNotificationAuthentication_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = smartIdClient.createNotificationAuthentication() + .withDocumentNumber(DOCUMENT_NUMBER) + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Login?"))) + .initAuthenticationSession(); + + assertNotNull(response.sessionID()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class NotificationBasedSignatureSession { + + @Test + void createNotificationSignature_withDocumentNumber() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(64).getBytes()); + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertSessionResponse(response); + } + + @Test + void createNotificationSignature_withSemanticsIdentifier() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/notification/etsi/PNOEE-1234567890", + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(64).getBytes()); + NotificationSignatureSessionResponse response = smartIdClient.createNotificationSignature() + .withSemanticsIdentifier(new SemanticsIdentifier(PERSON_CODE)) + .withInteractions(List.of(NotificationInteraction.confirmationMessage("Sign it!"))) + .withSignableHash(signableHash) + .initSignatureSession(); + + assertSessionResponse(response); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationBasedSignatureSession { + + private static final String DOCUMENT_NUMBER = "PNOEE-1234567890-MOCK-Q"; + + @Test + void createLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) + .initSignatureSession(); + + assertNotNull(response); + } + + @Test + void createLinkedNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/notification/linked/" + DOCUMENT_NUMBER, + "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", + "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse response = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(new SignableData("Test data".getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withLinkedSessionID("10000000-0000-000-000-000000000000") + .withNonce("cmFuZG9tTm9uY2U=") + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign?"))) + .withShareMdClientIpAddress(true) + .initSignatureSession(); + + assertNotNull(response); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class SessionsStatus { + + @Test + void fetchFinalSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-successful-authentication.json"); + + SessionStatus status = smartIdClient.getSessionStatusPoller().fetchFinalSessionStatus("abcdef1234567890"); + + assertEquals("COMPLETE", status.getState()); + assertEquals("OK", status.getResult().getEndResult()); + } + + @Test + void getSessionStatus() { + SmartIdRestServiceStubs.stubRequestWithResponse("/session/abcdef1234567890", "responses/session-status-running.json"); + + SessionStatus status = smartIdClient.getSessionStatusPoller().getSessionStatus("abcdef1234567890"); + + assertEquals("RUNNING", status.getState()); + assertNull(status.getResult()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForAuth { + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_authenticationForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest request = builder.getAuthenticationSessionRequest(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInitialCallbackUrl(request.initialCallbackUrl()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.AUTHENTICATION, deviceLinkType, response.sessionToken()); + } + + @Test + void createDynamicContent_authenticationWithQRCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(qrCodeUri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_authenticationWithQRCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/authentication/device-link/anonymous", + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient.createDeviceLinkAuthentication() + .withRpChallenge(Base64.toBase64String("a".repeat(32).getBytes())) + .withSignatureAlgorithm(SignatureAlgorithm.RSASSA_PSS) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Log in?"))) + .withHashAlgorithm(HashAlgorithm.SHA3_512); + DeviceLinkSessionResponse response = builder.initAuthenticationSession(); + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .withDigest("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=") + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.AUTHENTICATION, DeviceLinkType.QR_CODE, response.sessionToken()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForSignature { + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_sameDevice(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-same-device.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.SIGNATURE, deviceLinkType, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCode() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + assertUri(qrCodeUri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_withQrCodeImage() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse("/signature/device-link/document/PNOEE-1234567890-MOCK-Q", + "requests/sign/device-link/signature/device-link-signature-request-qr-code.json", + "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + var signableHash = new SignableHash("a".repeat(32).getBytes()); + + DeviceLinkSignatureSessionRequestBuilder builder = smartIdClient.createDeviceLinkSignature() + .withDocumentNumber(DOCUMENT_NUMBER) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign document?"))) + .withSignableHash(signableHash); + DeviceLinkSessionResponse response = builder.initSignatureSession(); + DeviceLinkSignatureSessionRequest request = builder.getSignatureSessionRequest(); + + Duration elapsed = Duration.between(response.receivedAt(), Instant.now()); + URI qrCodeUri = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withElapsedSeconds(elapsed.getSeconds()) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withDigest(signableHash.getDigestInBase64()) + .withInteractions(request.interactions()) + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.SIGNATURE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DynamicContentForCertificateChoice { + + @Test + void createDynamicContent_certificateChoiceWithDeviceLinkGeneratedForQrCode() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLink, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @Test + void createDynamicContent_createQrCodeImage() { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withNonce(Base64.toBase64String("randomNonce".getBytes())) + .withCertificateLevel(CertificateLevel.ADVANCED) + .initCertificateChoice(); + + long elapsedSeconds = Duration.between(response.receivedAt(), Instant.now()).getSeconds(); + + URI qrCodeUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withElapsedSeconds(elapsedSeconds) + .withLang("eng") + .buildDeviceLink(response.sessionSecret()); + + String qrCodeDataUri = QrCodeGenerator.generateDataUri(qrCodeUri.toString()); + String[] qrCodeDataUriParts = qrCodeDataUri.split(","); + URI uri = URI.create(QrCodeUtil.extractQrContent(qrCodeDataUriParts[1]).getText()); + + assertUri(uri, SessionType.CERTIFICATE_CHOICE, DeviceLinkType.QR_CODE, response.sessionToken()); + } + + @ParameterizedTest + @EnumSource(value = DeviceLinkType.class, names = {"WEB_2_APP", "APP_2_APP"}) + void createDynamicContent_certificateChoiceForSameDeviceFlows(DeviceLinkType deviceLinkType) { + SmartIdRestServiceStubs.stubRequestWithResponse("/signature/certificate-choice/device-link/anonymous", + "requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json", + "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkSessionResponse response = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withInitialCallbackUrl(INITIAL_CALLBACK_URL) + .initCertificateChoice(); + + URI deviceLinkUri = smartIdClient.createDynamicContent() + .withDeviceLinkBase(response.deviceLinkBase().toString()) + .withDeviceLinkType(deviceLinkType) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(response.sessionToken()) + .withLang("eng") + .withInitialCallbackUrl("https://smart-id.com/callback") + .buildDeviceLink(response.sessionSecret()); + + assertUri(deviceLinkUri, SessionType.CERTIFICATE_CHOICE, deviceLinkType, response.sessionToken()); + } + } + + private static void assertUri(URI qrCodeUri, SessionType sessionType, DeviceLinkType deviceLinkType, String sessionToken) { + assertEquals("https", qrCodeUri.getScheme()); + assertEquals("smart-id.com", qrCodeUri.getHost()); + assertEquals("/device-link/", qrCodeUri.getPath()); + + assertTrue(qrCodeUri.getQuery().contains("version=1.0")); + assertTrue(qrCodeUri.getQuery().contains("sessionType=" + sessionType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("deviceLinkType=" + deviceLinkType.getValue())); + assertTrue(qrCodeUri.getQuery().contains("sessionToken=" + sessionToken)); + assertTrue(qrCodeUri.getQuery().contains("lang=eng")); + assertTrue(qrCodeUri.getQuery().contains("authCode=")); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java index 16a9f71c..5bfbd684 100644 --- a/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoCondition.java @@ -1,52 +1,52 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.lang.reflect.AnnotatedElement; -import java.util.Optional; - -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; -import org.junit.jupiter.api.extension.ExtensionContext; - -public class SmartIdDemoCondition implements ExecutionCondition { - - /** - * Allows switching off tests going against smart-id demo env. - * This is sometimes needed if the test data in smart-id is temporarily broken. - */ - private static final boolean TEST_AGAINST_SMART_ID_DEMO = true; - - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - Optional element = context.getElement(); - if (element.isPresent() && element.get().isAnnotationPresent(SmartIdDemoIntegrationTest.class) && !TEST_AGAINST_SMART_ID_DEMO) { - return ConditionEvaluationResult.disabled("Running against Smart-ID demo is turned off"); - } - return ConditionEvaluationResult.enabled("Running against Smart-ID demo is turned on"); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SmartIdDemoCondition implements ExecutionCondition { + + /** + * Allows switching off tests going against smart-id demo env. + * This is sometimes needed if the test data in smart-id is temporarily broken. + */ + private static final boolean TEST_AGAINST_SMART_ID_DEMO = true; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional element = context.getElement(); + if (element.isPresent() && element.get().isAnnotationPresent(SmartIdDemoIntegrationTest.class) && !TEST_AGAINST_SMART_ID_DEMO) { + return ConditionEvaluationResult.disabled("Running against Smart-ID demo is turned off"); + } + return ConditionEvaluationResult.enabled("Running against Smart-ID demo is turned on"); + } +} diff --git a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java index 2e1439a5..14a5fc74 100644 --- a/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/SmartIdDemoIntegrationTest.java @@ -1,40 +1,40 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2024 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.junit.jupiter.api.extension.ExtendWith; - -@Target({ElementType.TYPE, ElementType.METHOD}) // Can be applied to classes or methods -@Retention(RetentionPolicy.RUNTIME) -@ExtendWith(SmartIdDemoCondition.class) -public @interface SmartIdDemoIntegrationTest { -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2024 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target({ElementType.TYPE, ElementType.METHOD}) // Can be applied to classes or methods +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SmartIdDemoCondition.class) +public @interface SmartIdDemoIntegrationTest { +} diff --git a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java index 4d2fc0c8..66b6118b 100644 --- a/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java +++ b/src/test/java/ee/sk/smartid/SmartIdRestServiceStubs.java @@ -1,153 +1,153 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; - -public class SmartIdRestServiceStubs { - - public static void stubNotFoundResponse(String urlEquals) { - stubFor(get(urlEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } - - public static void stubPostRequestWithResponse(String url, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubNotFoundResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 404); - } - - public static void stubUnauthorizedResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 401); - } - - public static void stubBadRequestResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 400); - } - - public static void stubForbiddenResponse(String url, String requestFile) { - stubErrorResponse(url, requestFile, 403); - } - - public static void stubErrorResponse(String url, String requestFile, int errorStatus) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) - .willReturn(aResponse() - .withStatus(errorStatus) - .withHeader("Content-Type", "application/json") - .withBody("Not found"))); - } - - public static void stubRequestWithResponse(String urlEquals, String responseFile) { - stubFor(get(urlPathEqualTo(urlEquals)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubStrictRequestWithResponse(String url, String requestFile, String responseFile) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .withRequestBody(equalToJson(readFileBody(requestFile), false, false)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile)))); - } - - public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { - String urlEquals = "/session/" + sessionId; - stubFor(get(urlEqualTo(urlEquals)) - .inScenario("session status") - .whenScenarioStateIs(startState) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(readFileBody(responseFile))) - .willSetStateTo(endState) - ); - } - - public static void stubPostErrorResponse(String url, int errorStatus) { - stubFor(post(urlEqualTo(url)) - .withHeader("Accept", equalTo("application/json")) - .willReturn(aResponse() - .withStatus(errorStatus) - .withHeader("Content-Type", "application/json") - .withBody(""))); - } - - private static String readFileBody(String fileName) { - ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); - URL resource = classLoader.getResource(fileName); - assertNotNull(resource, "File not found: " + fileName); - File file = new File(resource.getFile()); - try { - return Files.readString(file.toPath()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class SmartIdRestServiceStubs { + + public static void stubNotFoundResponse(String urlEquals) { + stubFor(get(urlEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } + + public static void stubPostRequestWithResponse(String url, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubNotFoundResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 404); + } + + public static void stubUnauthorizedResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 401); + } + + public static void stubBadRequestResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 400); + } + + public static void stubForbiddenResponse(String url, String requestFile) { + stubErrorResponse(url, requestFile, 403); + } + + public static void stubErrorResponse(String url, String requestFile, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody("Not found"))); + } + + public static void stubRequestWithResponse(String urlEquals, String responseFile) { + stubFor(get(urlPathEqualTo(urlEquals)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), true, true)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubStrictRequestWithResponse(String url, String requestFile, String responseFile) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(equalToJson(readFileBody(requestFile), false, false)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile)))); + } + + public static void stubSessionStatusWithState(String sessionId, String responseFile, String startState, String endState) { + String urlEquals = "/session/" + sessionId; + stubFor(get(urlEqualTo(urlEquals)) + .inScenario("session status") + .whenScenarioStateIs(startState) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(readFileBody(responseFile))) + .willSetStateTo(endState) + ); + } + + public static void stubPostErrorResponse(String url, int errorStatus) { + stubFor(post(urlEqualTo(url)) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(errorStatus) + .withHeader("Content-Type", "application/json") + .withBody(""))); + } + + private static String readFileBody(String fileName) { + ClassLoader classLoader = SmartIdRestServiceStubs.class.getClassLoader(); + URL resource = classLoader.getResource(fileName); + assertNotNull(resource, "File not found: " + fileName); + File file = new File(resource.getFile()); + try { + return Files.readString(file.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java index 4b7c2dee..47e0860d 100644 --- a/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java +++ b/src/test/java/ee/sk/smartid/UserRefusedInteractionArgumentsProvider.java @@ -1,48 +1,48 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import java.util.stream.Stream; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; - -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; -import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; -import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; - -public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), - Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), - Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageException; +import ee.sk.smartid.exception.useraction.UserRefusedConfirmationMessageWithVerificationChoiceException; +import ee.sk.smartid.exception.useraction.UserRefusedDisplayTextAndPinException; + +public class UserRefusedInteractionArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("displayTextAndPIN", UserRefusedDisplayTextAndPinException.class), + Arguments.of("confirmationMessage", UserRefusedConfirmationMessageException.class), + Arguments.of("confirmationMessageAndVerificationCodeChoice", UserRefusedConfirmationMessageWithVerificationChoiceException.class)); + } +} diff --git a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java index f41876a6..847e2fa7 100644 --- a/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java +++ b/src/test/java/ee/sk/smartid/VerificationCodeCalculatorTest.java @@ -1,83 +1,83 @@ -package ee.sk.smartid; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - - -public class VerificationCodeCalculatorTest { - - @Test - public void calculate_ok() { - byte[] dummyDocumentHash = new byte[]{27, -69}; - String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); - assertEquals("4555", verificationCode); - } - - @ParameterizedTest - @ArgumentsSource(VerificationCodeCalculatorArgumentProvider.class) - public void calculate_generateCorrectVerificationCodes(String expectedVerificationCode, String inputString) { - byte[] hash = DigestCalculator.calculateDigest(inputString.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); - assertEquals(expectedVerificationCode, VerificationCodeCalculator.calculate(hash)); - } - - @ParameterizedTest - @NullAndEmptySource - public void calculate_withEmptyInput_throwsException(byte[] data) { - var ex = assertThrows(SmartIdClientException.class, () -> VerificationCodeCalculator.calculate(data)); - assertEquals("Parameter 'data' cannot be empty", ex.getMessage()); - } - - private static class VerificationCodeCalculatorArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of("7712", "Hello World!"), - Arguments.of("4612", "Hedgehogs – why can't they just share the hedge?"), - Arguments.of("7782", "Go ahead, make my day."), - Arguments.of("1464", "You're gonna need a bigger boat."), - Arguments.of("4240", "Say 'hello' to my little friend!") - ); - } - } -} +package ee.sk.smartid; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + + +public class VerificationCodeCalculatorTest { + + @Test + public void calculate_ok() { + byte[] dummyDocumentHash = new byte[]{27, -69}; + String verificationCode = VerificationCodeCalculator.calculate(dummyDocumentHash); + assertEquals("4555", verificationCode); + } + + @ParameterizedTest + @ArgumentsSource(VerificationCodeCalculatorArgumentProvider.class) + public void calculate_generateCorrectVerificationCodes(String expectedVerificationCode, String inputString) { + byte[] hash = DigestCalculator.calculateDigest(inputString.getBytes(StandardCharsets.UTF_8), HashAlgorithm.SHA_256); + assertEquals(expectedVerificationCode, VerificationCodeCalculator.calculate(hash)); + } + + @ParameterizedTest + @NullAndEmptySource + public void calculate_withEmptyInput_throwsException(byte[] data) { + var ex = assertThrows(SmartIdClientException.class, () -> VerificationCodeCalculator.calculate(data)); + assertEquals("Parameter 'data' cannot be empty", ex.getMessage()); + } + + private static class VerificationCodeCalculatorArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("7712", "Hello World!"), + Arguments.of("4612", "Hedgehogs – why can't they just share the hedge?"), + Arguments.of("7782", "Go ahead, make my day."), + Arguments.of("1464", "You're gonna need a bigger boat."), + Arguments.of("4240", "Say 'hello' to my little friend!") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java index 081927ed..7f26c056 100644 --- a/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/NonQualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -1,167 +1,167 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.InvalidCertificateGenerator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class NonQualifiedAuthenticationCertificatePurposeValidatorTest { - - private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); - private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); - private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; - private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; - - private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; - - @BeforeEach - void setUp() { - purposeValidator = new NonQualifiedAuthenticationCertificatePurposeValidator(); - } - - @Test - void validate_ok() { - purposeValidator.validate(NQ_AUTH_CERT); - } - - @Test - void validate_certificateNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); - assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); - } - - @Test - void validate_extendedKeyUsageIsMissing_throwException() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(null) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_invalidExtendedKeyProvided_throwException() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageIsMissing() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageNotSmartIdAuth() { - CertificatePolicies policies = toNonQualifiedAuthCertificate(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .withKeyUsage(keyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_certificateCannotBeUsedForAuthentication_throwException() { - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - private static CertificatePolicies toNonQualifiedAuthCertificate() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_POLICY_OID), - new DERSequence() - ); - return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class NonQualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate NQ_AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-auth-cert-40504049999.crt")); + private static final X509Certificate NQ_SIGN_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/nq-signing-cert.pem")); + private static final String SK_NON_QUALIFIED_POLICY_OID = "1.3.6.1.4.1.10015.17.1"; + private static final String NCP_POLICY_OID = "0.4.0.2042.1.1"; + + private NonQualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new NonQualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_ok() { + purposeValidator.validate(NQ_AUTH_CERT); + } + + @Test + void validate_certificateNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate does not have certificate policy OIDs and is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate certificate = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Certificate is not a non-qualified Smart-ID certificate", ex.getMessage()); + } + + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toNonQualifiedAuthCertificate(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_certificateCannotBeUsedForAuthentication_throwException() { + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(NQ_SIGN_CERT)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + private static CertificatePolicies toNonQualifiedAuthCertificate() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_NON_QUALIFIED_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } +} diff --git a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java index d35e250e..15aadb63 100644 --- a/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java +++ b/src/test/java/ee/sk/smartid/auth/QualifiedAuthenticationCertificatePurposeValidatorTest.java @@ -1,170 +1,170 @@ -package ee.sk.smartid.auth; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.X509Certificate; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x509.CertificatePolicies; -import org.bouncycastle.asn1.x509.ExtendedKeyUsage; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.PolicyInformation; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.FileUtil; -import ee.sk.smartid.InvalidCertificateGenerator; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class QualifiedAuthenticationCertificatePurposeValidatorTest { - - private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt")); - private static final X509Certificate AUTH_CERT_BEFORE_APRIL_2025 = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-pnolv-020100-29990-mock-q.crt")); - private static final String SK_QUALIFIED_AUTH_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; - private static final String NCP_PLUS_POLICY_OID = "0.4.0.2042.1.2"; - - private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; - - @BeforeEach - void setUp() { - purposeValidator = new QualifiedAuthenticationCertificatePurposeValidator(); - } - - @Test - void validate_authCert_afterApril2025_ok() { - assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); - } - - // TODO - 23.09.25: Will leave it for now, as change might be needed for automated testing. - @Disabled("Test-certificate was created with 1.3.6.1.4.1.10015.3.17.2 and conflicts with required value 1.3.6.1.4.1.10015.17.2") - @Test - void validate_authCert_beforeApril2025_ok() { - assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT_BEFORE_APRIL_2025)); - } - - @Test - void validate_certificateIsNotProvided_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); - assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); - } - - @Test - void validate_certificatePoliciesAreMissing_throwException() { - X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); - assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - @Test - void validate_invalidCertificatePolicies_throwException() { - String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; - PolicyInformation policyInfo = new PolicyInformation( - new ASN1ObjectIdentifier(invalidPolicyOid), - new DERSequence() - ); - CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); - X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); - assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); - } - - @Test - void validate_extendedKeyUsageIsMissing_throwException() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(null) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_invalidExtendedKeyProvided_throwException() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageIsMissing() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - @Test - void validate_keyUsageNotSmartIdAuth() { - CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); - KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); - ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withPolicies(policies) - .withExtendedKeyUsage(extendedKeyUsage) - .withKeyUsage(keyUsage) - .createCertificate(); - - var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); - assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); - } - - private static CertificatePolicies toQualifiedSmartIdAuthPolicy() { - PolicyInformation skQPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(SK_QUALIFIED_AUTH_POLICY_OID), - new DERSequence() - ); - PolicyInformation ncpPolicy = new PolicyInformation( - new ASN1ObjectIdentifier(NCP_PLUS_POLICY_OID), - new DERSequence() - ); - return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); - } -} +package ee.sk.smartid.auth; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.X509Certificate; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.FileUtil; +import ee.sk.smartid.InvalidCertificateGenerator; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class QualifiedAuthenticationCertificatePurposeValidatorTest { + + private static final X509Certificate AUTH_CERT = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-cert-40504040001-demo-q.crt")); + private static final X509Certificate AUTH_CERT_BEFORE_APRIL_2025 = CertificateUtil.toX509Certificate(FileUtil.readFileToString("test-certs/auth-pnolv-020100-29990-mock-q.crt")); + private static final String SK_QUALIFIED_AUTH_POLICY_OID = "1.3.6.1.4.1.10015.17.2"; + private static final String NCP_PLUS_POLICY_OID = "0.4.0.2042.1.2"; + + private QualifiedAuthenticationCertificatePurposeValidator purposeValidator; + + @BeforeEach + void setUp() { + purposeValidator = new QualifiedAuthenticationCertificatePurposeValidator(); + } + + @Test + void validate_authCert_afterApril2025_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT)); + } + + // TODO - 23.09.25: Will leave it for now, as change might be needed for automated testing. + @Disabled("Test-certificate was created with 1.3.6.1.4.1.10015.3.17.2 and conflicts with required value 1.3.6.1.4.1.10015.17.2") + @Test + void validate_authCert_beforeApril2025_ok() { + assertDoesNotThrow(() -> purposeValidator.validate(AUTH_CERT_BEFORE_APRIL_2025)); + } + + @Test + void validate_certificateIsNotProvided_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> purposeValidator.validate(null)); + assertEquals("Parameter 'certificate' is not provided", ex.getMessage()); + } + + @Test + void validate_certificatePoliciesAreMissing_throwException() { + X509Certificate cert = InvalidCertificateGenerator.builder().createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate does not have certificate policy OIDs and is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + @Test + void validate_invalidCertificatePolicies_throwException() { + String invalidPolicyOid = "1.3.6.1.4.1.99999.1"; + PolicyInformation policyInfo = new PolicyInformation( + new ASN1ObjectIdentifier(invalidPolicyOid), + new DERSequence() + ); + CertificatePolicies policies = InvalidCertificateGenerator.createCertificatePolicies(policyInfo); + X509Certificate cert = InvalidCertificateGenerator.builder().withPolicies(policies).createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(cert)); + assertEquals("Certificate is not a qualified Smart-ID authentication certificate", ex.getMessage()); + } + + @Test + void validate_extendedKeyUsageIsMissing_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(null) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_invalidExtendedKeyProvided_throwException() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_smartcardlogon); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageIsMissing() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + @Test + void validate_keyUsageNotSmartIdAuth() { + CertificatePolicies policies = toQualifiedSmartIdAuthPolicy(); + KeyUsage keyUsage = new KeyUsage(KeyUsage.nonRepudiation); + ExtendedKeyUsage extendedKeyUsage = new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth); + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withPolicies(policies) + .withExtendedKeyUsage(extendedKeyUsage) + .withKeyUsage(keyUsage) + .createCertificate(); + + var ex = assertThrows(UnprocessableSmartIdResponseException.class, () -> purposeValidator.validate(certificate)); + assertEquals("Provided certificate cannot be used for authentication", ex.getMessage()); + } + + private static CertificatePolicies toQualifiedSmartIdAuthPolicy() { + PolicyInformation skQPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(SK_QUALIFIED_AUTH_POLICY_OID), + new DERSequence() + ); + PolicyInformation ncpPolicy = new PolicyInformation( + new ASN1ObjectIdentifier(NCP_PLUS_POLICY_OID), + new DERSequence() + ); + return InvalidCertificateGenerator.createCertificatePolicies(skQPolicy, ncpPolicy); + } +} diff --git a/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java index 9338fdd1..03730941 100644 --- a/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java +++ b/src/test/java/ee/sk/smartid/common/InteractionValidatorTest.java @@ -1,73 +1,73 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.stream.Stream; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class InteractionValidatorTest { - - @ParameterizedTest - @MethodSource("getValidDisplayTextForInteraction") - void validate_deviceLinkInteraction_ok(String displayText) { - assertDoesNotThrow(() -> InteractionValidator.validate(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); - } - - @ParameterizedTest - @MethodSource("getValidDisplayTextForInteraction") - void validate_notificationInteraction_ok(String displayText) { - assertDoesNotThrow(() -> InteractionValidator.validate(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); - } - - @ParameterizedTest - @MethodSource("getInvalidConfirmationMessageDisplayText") - void validate_interactionWithInvalidDisplayTextLength_throwException(String displayText, String expectedMessage) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> InteractionValidator.validate(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, displayText)); - assertEquals(expectedMessage, ex.getMessage()); - } - - public static Stream getValidDisplayTextForInteraction() { - return Stream.of("a", "a".repeat(60)).map(Arguments::of); - } - - public static Stream getInvalidConfirmationMessageDisplayText() { - return Stream.of(Arguments.of(null, "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), - Arguments.of("", "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), - Arguments.of("a".repeat(201), "Value for 'displayText200' must not exceed 200 characters")); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class InteractionValidatorTest { + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_deviceLinkInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getValidDisplayTextForInteraction") + void validate_notificationInteraction_ok(String displayText) { + assertDoesNotThrow(() -> InteractionValidator.validate(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, displayText)); + } + + @ParameterizedTest + @MethodSource("getInvalidConfirmationMessageDisplayText") + void validate_interactionWithInvalidDisplayTextLength_throwException(String displayText, String expectedMessage) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> InteractionValidator.validate(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, displayText)); + assertEquals(expectedMessage, ex.getMessage()); + } + + public static Stream getValidDisplayTextForInteraction() { + return Stream.of("a", "a".repeat(60)).map(Arguments::of); + } + + public static Stream getInvalidConfirmationMessageDisplayText() { + return Stream.of(Arguments.of(null, "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("", "Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'"), + Arguments.of("a".repeat(201), "Value for 'displayText200' must not exceed 200 characters")); + } +} diff --git a/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java index 4273ba2a..286c4c54 100644 --- a/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java +++ b/src/test/java/ee/sk/smartid/common/InteractionsMapperTest.java @@ -1,88 +1,88 @@ -package ee.sk.smartid.common; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.rest.dao.Interaction; - -class InteractionsMapperTest { - - @Test - void from_deviceLinkInteraction() { - DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_deviceLinkInteractionsList() { - DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); - - assertFalse(interactions.isEmpty()); - Interaction interaction = interactions.get(0); - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_notificationInteraction() { - NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @Test - void from_notificationInteractionsList() { - NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); - List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); - - assertFalse(interactions.isEmpty()); - Interaction interaction = interactions.get(0); - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } -} +package ee.sk.smartid.common; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.rest.dao.Interaction; + +class InteractionsMapperTest { + + @Test + void from_deviceLinkInteraction() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_deviceLinkInteractionsList() { + DeviceLinkInteraction deviceLinkInteraction = new DeviceLinkInteraction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteraction() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + Interaction interaction = InteractionsMapper.from(deviceLinkInteraction); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @Test + void from_notificationInteractionsList() { + NotificationInteraction deviceLinkInteraction = new NotificationInteraction(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, "Log in?", null); + List interactions = InteractionsMapper.from(List.of(deviceLinkInteraction)); + + assertFalse(interactions.isEmpty()); + Interaction interaction = interactions.get(0); + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java index 0b1ebe40..69adad3c 100644 --- a/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java +++ b/src/test/java/ee/sk/smartid/common/devicelink/UrlSafeTokenGeneratorTest.java @@ -1,77 +1,77 @@ -package ee.sk.smartid.common.devicelink; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.regex.Pattern; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class UrlSafeTokenGeneratorTest { - - @Test - void random() { - String random = UrlSafeTokenGenerator.random(); - - assertTrue(random.length() >= 22 && random.length() <= 86); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @Test - void ofLength() { - String random = UrlSafeTokenGenerator.ofLength(22); - - assertEquals(22, random.length()); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @Test - void randomBetween() { - String random = UrlSafeTokenGenerator.randomBetween(22, 24); - - assertTrue(random.length() >= 22 && random.length() <= 24); - assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); - } - - @ParameterizedTest - @CsvSource({ - "21, 86", // min length smaller than allowed - "22, 87", // max length larger than allowed - "86, 22" // min length larger than max length - }) - void randomBetween(int minLength, int maxLength) { - var ex = assertThrows(SmartIdClientException.class, () -> UrlSafeTokenGenerator.randomBetween(minLength, maxLength)); - assertEquals("Length must be between 22 and 86 chars", ex.getMessage()); - } -} +package ee.sk.smartid.common.devicelink; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class UrlSafeTokenGeneratorTest { + + @Test + void random() { + String random = UrlSafeTokenGenerator.random(); + + assertTrue(random.length() >= 22 && random.length() <= 86); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void ofLength() { + String random = UrlSafeTokenGenerator.ofLength(22); + + assertEquals(22, random.length()); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @Test + void randomBetween() { + String random = UrlSafeTokenGenerator.randomBetween(22, 24); + + assertTrue(random.length() >= 22 && random.length() <= 24); + assertTrue(Pattern.matches("^[A-Za-z0-9_-]+$", random)); + } + + @ParameterizedTest + @CsvSource({ + "21, 86", // min length smaller than allowed + "22, 87", // max length larger than allowed + "86, 22" // min length larger than max length + }) + void randomBetween(int minLength, int maxLength) { + var ex = assertThrows(SmartIdClientException.class, () -> UrlSafeTokenGenerator.randomBetween(minLength, maxLength)); + assertEquals("Length must be between 22 and 86 chars", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java index 935e719e..b5378658 100644 --- a/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java +++ b/src/test/java/ee/sk/smartid/common/devicelink/interactions/DeviceLinkInteractionTest.java @@ -1,100 +1,100 @@ -package ee.sk.smartid.common.devicelink.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class DeviceLinkInteractionTest { - - @Nested - class DisplayTextAndPin { - - @Test - void displayTextAndPin_ok() { - DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPin("Log in?"); - - assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void displayTextAndPin_textIsEmpty_throwException(String displayText) { - var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin(displayText)); - assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); - } - - @Test - void displayTextAndPin_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin("a".repeat(61))); - assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessage { - - @Test - void confirmationMessage() { - DeviceLinkInteraction interaction = DeviceLinkInteraction.confirmationMessage("Log in?"); - - assertEquals(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessage_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); - } - - @Test - void confirmationMessage_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Test - void instantiateDeviceLinkWithNullValues_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkInteraction(null, null, null)); - assertEquals("Value for 'type' must be set", ex.getMessage()); - } -} +package ee.sk.smartid.common.devicelink.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class DeviceLinkInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.displayTextAndPin("Log in?"); + + assertEquals(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> DeviceLinkInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage() { + DeviceLinkInteraction interaction = DeviceLinkInteraction.confirmationMessage("Log in?"); + + assertEquals(DeviceLinkInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> DeviceLinkInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateDeviceLinkWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new DeviceLinkInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java index fe1ca29e..eb9e3874 100644 --- a/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java +++ b/src/test/java/ee/sk/smartid/common/notification/interactions/NotificationInteractionTest.java @@ -1,125 +1,125 @@ -package ee.sk.smartid.common.notification.interactions; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; - -class NotificationInteractionTest { - - @Nested - class DisplayTextAndPin { - - @Test - void displayTextAndPin_ok() { - NotificationInteraction interaction = NotificationInteraction.displayTextAndPin("Log in?"); - - assertEquals(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); - assertEquals("Log in?", interaction.displayText60()); - assertNull(interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void displayTextAndPin_textIsEmpty_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin(displayText)); - assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); - } - - @Test - void displayTextAndPin_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin("a".repeat(61))); - assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessage { - - @Test - void confirmationMessage_ok() { - NotificationInteraction interaction = NotificationInteraction.confirmationMessage("Log in?"); - - assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessage_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); - } - - @Test - void confirmationMessage_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Nested - class ConfirmationMessageAndVerificationCodeChoice { - - @Test - void confirmationMessageAndVerificationCodeChoice_ok() { - NotificationInteraction interaction = NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Log in?"); - - assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, interaction.type()); - assertNull(interaction.displayText60()); - assertEquals("Log in?", interaction.displayText200()); - } - - @ParameterizedTest - @NullAndEmptySource - void confirmationMessageAndVerificationCodeChoice_emptyTextUsed_throwException(String displayText) { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice(displayText)); - assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'", ex.getMessage()); - } - - @Test - void confirmationMessageAndVerificationCodeChoice_textWithExceedingLength_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))); - assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); - } - } - - @Test - void instantiateNotificationInteractionWithNullValues_throwException() { - var ex = assertThrows(SmartIdRequestSetupException.class, () -> new NotificationInteraction(null, null, null)); - assertEquals("Value for 'type' must be set", ex.getMessage()); - } -} +package ee.sk.smartid.common.notification.interactions; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.exception.permanent.SmartIdRequestSetupException; + +class NotificationInteractionTest { + + @Nested + class DisplayTextAndPin { + + @Test + void displayTextAndPin_ok() { + NotificationInteraction interaction = NotificationInteraction.displayTextAndPin("Log in?"); + + assertEquals(NotificationInteractionType.DISPLAY_TEXT_AND_PIN, interaction.type()); + assertEquals("Log in?", interaction.displayText60()); + assertNull(interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void displayTextAndPin_textIsEmpty_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin(displayText)); + assertEquals("Value for 'displayText60' must be set when type is 'DISPLAY_TEXT_AND_PIN'", ex.getMessage()); + } + + @Test + void displayTextAndPin_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.displayTextAndPin("a".repeat(61))); + assertEquals("Value for 'displayText60' must not exceed 60 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessage { + + @Test + void confirmationMessage_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessage("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessage_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE'", ex.getMessage()); + } + + @Test + void confirmationMessage_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessage("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Nested + class ConfirmationMessageAndVerificationCodeChoice { + + @Test + void confirmationMessageAndVerificationCodeChoice_ok() { + NotificationInteraction interaction = NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Log in?"); + + assertEquals(NotificationInteractionType.CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE, interaction.type()); + assertNull(interaction.displayText60()); + assertEquals("Log in?", interaction.displayText200()); + } + + @ParameterizedTest + @NullAndEmptySource + void confirmationMessageAndVerificationCodeChoice_emptyTextUsed_throwException(String displayText) { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice(displayText)); + assertEquals("Value for 'displayText200' must be set when type is 'CONFIRMATION_MESSAGE_AND_VERIFICATION_CODE_CHOICE'", ex.getMessage()); + } + + @Test + void confirmationMessageAndVerificationCodeChoice_textWithExceedingLength_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> NotificationInteraction.confirmationMessageAndVerificationCodeChoice("a".repeat(201))); + assertEquals("Value for 'displayText200' must not exceed 200 characters", ex.getMessage()); + } + } + + @Test + void instantiateNotificationInteractionWithNullValues_throwException() { + var ex = assertThrows(SmartIdRequestSetupException.class, () -> new NotificationInteraction(null, null, null)); + assertEquals("Value for 'type' must be set", ex.getMessage()); + } +} diff --git a/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java index d0c49e07..4f5a2031 100644 --- a/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java +++ b/src/test/java/ee/sk/smartid/dao/SemanticsIdentifierTest.java @@ -1,60 +1,60 @@ -package ee.sk.smartid.dao; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.rest.dao.SemanticsIdentifier; - -public class SemanticsIdentifierTest { - - @Test - public void constructor1() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("AAA", "BB", "C123"); - - assertThat(semanticsIdentifier.getIdentifier(), is("AAABB-C123")); - } - - @Test - public void constructor2() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, "BB", "CCC"); - - assertThat(semanticsIdentifier.getIdentifier(), is("PNOBB-CCC")); - } - - @Test - public void constructor3() { - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "CCC-DDDDD"); - - assertThat(semanticsIdentifier.getIdentifier(), is("PNOLV-CCC-DDDDD")); - } - -} +package ee.sk.smartid.dao; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.rest.dao.SemanticsIdentifier; + +public class SemanticsIdentifierTest { + + @Test + public void constructor1() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("AAA", "BB", "C123"); + + assertThat(semanticsIdentifier.getIdentifier(), is("AAABB-C123")); + } + + @Test + public void constructor2() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, "BB", "CCC"); + + assertThat(semanticsIdentifier.getIdentifier(), is("PNOBB-CCC")); + } + + @Test + public void constructor3() { + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier(SemanticsIdentifier.IdentityType.PNO, SemanticsIdentifier.CountryCode.LV, "CCC-DDDDD"); + + assertThat(semanticsIdentifier.getIdentifier(), is("PNOLV-CCC-DDDDD")); + } + +} diff --git a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java index ebf7033c..b3619a6f 100644 --- a/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/ReadmeIntegrationTest.java @@ -1,949 +1,949 @@ -package ee.sk.smartid.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.time.Duration; -import java.time.Instant; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.AuthenticationCertificateLevel; -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.CertificateByDocumentNumberResult; -import ee.sk.smartid.CertificateChoiceResponse; -import ee.sk.smartid.CertificateChoiceResponseValidator; -import ee.sk.smartid.CertificateLevel; -import ee.sk.smartid.CertificateValidator; -import ee.sk.smartid.CertificateValidatorImpl; -import ee.sk.smartid.DeviceLinkAuthenticationResponseValidator; -import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; -import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; -import ee.sk.smartid.DeviceLinkType; -import ee.sk.smartid.FileTrustedCAStoreBuilder; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.NotificationAuthenticationResponseValidator; -import ee.sk.smartid.NotificationAuthenticationSessionRequestBuilder; -import ee.sk.smartid.QrCodeGenerator; -import ee.sk.smartid.RpChallenge; -import ee.sk.smartid.RpChallengeGenerator; -import ee.sk.smartid.SessionType; -import ee.sk.smartid.SignableData; -import ee.sk.smartid.SignatureCertificatePurposeValidator; -import ee.sk.smartid.SignatureCertificatePurposeValidatorFactory; -import ee.sk.smartid.SignatureCertificatePurposeValidatorFactoryImpl; -import ee.sk.smartid.SignatureResponse; -import ee.sk.smartid.SignatureResponseValidator; -import ee.sk.smartid.SignatureValueValidator; -import ee.sk.smartid.SignatureValueValidatorImpl; -import ee.sk.smartid.SmartIdClient; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.TrustedCACertStore; -import ee.sk.smartid.VerificationCodeCalculator; -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; -import ee.sk.smartid.common.notification.interactions.NotificationInteraction; -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.util.CallbackUrlUtil; - -@SmartIdDemoIntegrationTest -public class ReadmeIntegrationTest { - - private static final Pattern NUMERIC_PATTERN = Pattern.compile("^[0-9]{4}$"); - - private SmartIdClient smartIdClient; - - @BeforeEach - void setUp() { - smartIdClient = new SmartIdClient(); - smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); - smartIdClient.setRelyingPartyName("DEMO"); - smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); - - KeyStore keyStore = getKeystore(); - smartIdClient.setTrustStore(keyStore); - } - - @Disabled("Testing with device-link demo accounts is not possible at the moment") - @Nested - class DeviceLinkBasedExamples { - - @Nested - class Authentication { - - @Test - void anonymousAuthentication_withApp2App() { - // For security reasons a new hash value must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Create initial callback URL. - // Store the url-token only on backend side. Do not expose it to the client side. - // The url-token will be used to validate the callback request received from Smart-ID API - CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); - - // Setup builder - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - // to use anonymous authentication, do not set semantics identifier or document number - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )) - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link and in authCode - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.APP_2_APP) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withLang("est") - .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()) - .withInteractions(authenticationSessionRequest.interactions()) - .buildDeviceLink(sessionSecret); - - // Use the sessionId from the authentication session response to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - // The session can have different states such as RUNNING or COMPLETE. - // Check that the session has completed successfully - assertEquals("COMPLETE", sessionStatus.getState()); - - // Receive callback from Smart-ID API - // Extract query parameters from the callback URL received - Map queryParameters = Map.of("value", callbackUrl.urlToken(), "sessionSecretDigest", "asdjlaksdjklf", "userChallengeVerifier", "abachdfajklsfa"); - - // Validate there is active user session in the application with matching url-token - String tokenInUrl = queryParameters.get("value"); - - // Validate that sessionSecretDigest in the callback URL validates against sessionSecret from the init session response - CallbackUrlUtil.validateSessionSecretDigest(queryParameters.get("sessionSecretDigest"), sessionSecret); - - // Set up AuthenticationResponseValidator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); - // Validate the certificate and signature, then map the authentication response to the user's identity - AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate( - sessionStatus, - builder.getAuthenticationSessionRequest(), - queryParameters.get("userChallengeVerifier"), - "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withSemanticIdentifierAndQrCode() { - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // For security reasons a new rpChallenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withSemanticsIdentifier(semanticsIdentifier) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - - // Get authentication session request used for starting the authentication session and use it later to validate sessions status response - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // Use sessionID to start polling for session status - String sessionId = authenticationSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - // Build the device link URI (without the authCode parameter) - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withElapsedSeconds(elapsedSeconds) - .withInteractions(authenticationSessionRequest.interactions()) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETED", sessionStatus.getState()); - - // Validate the response and return user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withDocumentNumberAndQrCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // For security reasons a new rpChallenge must be created for each new authentication request - String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication session status OK response - - DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient - .createDeviceLinkAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge) - .withInteractions(Collections.singletonList( - DeviceLinkInteraction.displayTextAndPin("Log in?") - )); - - // Init authentication session - DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get AuthenticationSessionRequest after the request is made and store for validations - DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - String sessionId = authenticationSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - String sessionToken = authenticationSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = authenticationSessionResponse.sessionSecret(); - Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); - URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); - - // Generate the base (unprotected) device link URI, which does not yet include the authCode - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.AUTHENTICATION) - .withSessionToken(sessionToken) - .withDigest(rpChallenge) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withInteractions(authenticationSessionRequest.interactions()) - .withLang("est") - .buildDeviceLink(sessionSecret); - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETE", sessionStatus.getState()); - - // Validate the certificate and signature, then map the authentication response to the user's identity - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("EE", authenticationIdentity.getCountry()); - } - } - - @Nested - class Signature { - - @Test - void signature_withDocumentNumberAndQRCode() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - - // Build the device link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); - var deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QSCD) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withInteractions(signatureInteractions); - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); - // Get SignatureSessionRequest after the request is made and store for validations - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.sessionID(); - String sessionToken = signatureSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.sessionSecret(); - Instant receivedAt = signatureSessionResponse.receivedAt(); - URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); - - // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withSchemeName("smart-id-demo") - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .withInteractions(deviceLinkSignatureSessionRequest.interactions()) - .buildDeviceLink(sessionSecret); - - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - // Validate signature response - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - // Validate signature value - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - - @Test - void signature_withSemanticIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // Build the device link signature request - List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); - DeviceLinkSignatureSessionRequestBuilder deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(signatureInteractions); - - // Init signature session - DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); - // Get SignatureSessionRequest after the request is made and store for validations - DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); - - // Process the signature response - String signatureSessionId = signatureSessionResponse.sessionID(); - String sessionToken = signatureSessionResponse.sessionToken(); - - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = signatureSessionResponse.sessionSecret(); - Instant receivedAt = signatureSessionResponse.receivedAt(); - - // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse - // Start querying sessions status - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); - // Generate auth code - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase("smartid://") - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.SIGNATURE) - .withSessionToken(sessionToken) - .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .withInteractions(deviceLinkSignatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request - .buildDeviceLink(sessionSecret); - // Display QR-code to the user - - // Get the session status poller - poller = smartIdClient.getSessionStatusPoller(); - // Get signatureSessionId from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - // Validate signature response - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - // Validate signature value - SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); - signatureValueValidator.validate(signatureResponse.getSignatureValue(), - signableData.calculateHash(), - certificateChoiceResponse.getCertificate(), - signatureResponse.getRsaSsaPssParameters()); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); - assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - } - } - - @Nested - class NotificationBasedExamples { - - @Test - void authentication_withDocumentNumber() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // For security reasons a new rpChallenge must be created for each new authentication request - RpChallenge rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Generate verification code to be displayed to the user - String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - - NotificationAuthenticationSessionRequestBuilder builder = smartIdClient - .createNotificationAuthentication() - .withDocumentNumber(documentNumber) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))); - // Init authentication session - NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get notification-based authentication session request used for starting the authentication session - // and use it later to validate sessions status response - NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // SessionID is used to query sessions status later - String sessionId = authenticationSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Use sessionID from current session response to poll for session status - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - - // validate the sessions status and return user's identity - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = - NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void authentication_withSemanticIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // For security reasons a new RpChallenge must be created for each new authentication request - RpChallenge rpChallenge = RpChallengeGenerator.generate(); - // Store generated rpChallenge only on backend side. Do not expose it to the client side. - // Used for validating authentication sessions status OK response - - // Generate verification code to be displayed to the user - String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); - - NotificationAuthenticationSessionRequestBuilder builder = smartIdClient.createNotificationAuthentication() - .withSemanticsIdentifier(semanticIdentifier) - .withRpChallenge(rpChallenge.toBase64EncodedValue()) - .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) - .withInteractions(Collections.singletonList( - NotificationInteraction.displayTextAndPin("Log in?"))); - - // Init authentication session - NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); - // Get notification-based authentication session request used for starting the authentication session - // and use it later to validate sessions status response - NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); - - // SessionID is used to query sessions status later - String sessionId = authenticationSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - // Use sessionID from current session response to poll for session status - SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); - - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - AuthenticationIdentity authenticationIdentity = - NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) - .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); - - assertEquals("40504040001", authenticationIdentity.getIdentityCode()); - assertEquals("OK", authenticationIdentity.getGivenName()); - assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); - assertEquals("LT", authenticationIdentity.getCountry()); - } - - @Test - void certificateChoice_withSemanticIdentifier() { - var semanticsIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - // Use requested certificate level to validate certificate choice session status OK response. - CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticsIdentifier) - .withCertificateLevel(requestedCertificateLevel) - .initCertificateChoice(); - - String sessionId = certificateChoiceSessionResponse.sessionID(); - // SessionID is used to query sessions status later - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus sessionStatus = poller.getSessionStatus(sessionId); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); - - assertEquals("OK", response.getEndResult()); - assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); - assertNotNull(response.getCertificate()); - assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); - } - - @Test - void signature_withSemanticsIdentifier() { - var semanticIdentifier = new SemanticsIdentifier( - // 3 character identity type - // (PAS-passport, IDC-national identity card or PNO - (national) personal number) - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code - "40504040001"); // identifier (according to country and identity type reference) - - CertificateLevel certificateLevel = CertificateLevel.QSCD; - NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient - .createNotificationCertificateChoice() - .withSemanticsIdentifier(semanticIdentifier) - .withCertificateLevel(certificateLevel) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" - .initCertificateChoice(); - - // SessionID is used to query sessions status later - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Querying the sessions status - SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); - - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus, certificateLevel); - // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - // Create the Semantics Identifier - var semanticsIdentifier = new SemanticsIdentifier( - SemanticsIdentifier.IdentityType.PNO, - SemanticsIdentifier.CountryCode.EE, - "40504040001" - ); - - NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(certificateLevel) - .withSignableData(signableData) - .withSemanticsIdentifier(semanticsIdentifier) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the document")) - ) - .initSignatureSession(); - - // Get the session ID and continue to querying session status - String sessionID = signatureSessionResponse.sessionID(); - - // Display verification code to the user - String verificationCode = signatureSessionResponse.vc().value(); - assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); - - // Get sessionID from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals("PNOEE-40504040001-DEMO-Q", signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); - assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - - @Test - void signature_withDocumentNumber() { - String documentNumber = "PNOEE-40504040001-DEMO-Q"; - - CertificateLevel certificateLevel = CertificateLevel.QSCD; - // Query the certificate by document number to be used for creating the DataToSign - CertificateByDocumentNumberResult certificateByDocumentNumber = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .withCertificateLevel(certificateLevel) - .getCertificateByDocumentNumber(); - - // Set up the certificate validator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - // Validate the certificate is trusted and active - certificateValidator.validate(certificateByDocumentNumber.certificate()); - - // Validate the certificate is suitable for signing - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); - SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateByDocumentNumber.certificateLevel()); - certificatePurposeValidator.validate(certificateByDocumentNumber.certificate()); - - // For example use digidoc4j with SignatureBuilder to create DataToSign using `certificateByDocumentNumber.certificate()` - - // Create the signable data - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); - - NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() - .withCertificateLevel(certificateLevel) - .withSignableData(signableData) - .withDocumentNumber(documentNumber) - .withInteractions(List.of( - NotificationInteraction.confirmationMessage("Please sign the document")) - ) - .initSignatureSession(); - - // Get the session ID and continue to querying session status - String signatureSessionId = signatureSessionResponse.sessionID(); - - // Display verification code to the user - String verificationCode = signatureSessionResponse.vc().value(); - assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); - - // Get the session status poller - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - - // Get sessionID from current session response and poll for session status - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); - // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) - assertEquals("COMPLETE", signatureSessionStatus.getState()); - - SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); - - assertEquals("OK", signatureResponse.getEndResult()); - assertEquals(documentNumber, signatureResponse.getDocumentNumber()); - assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); - assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); - assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); - assertNotNull(signatureResponse.getCertificate()); - } - } - - @Nested - class CertificateByDocumentNumberExamples { - - @Test - void queryCertificate() { - String documentNumber = "PNOLT-40504040001-MOCK-Q"; - - // Build the certificate by document number request and query the certificate - CertificateByDocumentNumberResult certResponse = smartIdClient - .createCertificateByDocumentNumber() - .withDocumentNumber(documentNumber) - .getCertificateByDocumentNumber(); - - // Set up the certificate validator - TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); - CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); - - // Validate the certificate - certificateValidator.validate(certResponse.certificate()); - - // Validate the certificate is suitable for signing - SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); - SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certResponse.certificateLevel()); - certificatePurposeValidator.validate(certResponse.certificate()); - } - } - - @Disabled("Testing with device-link demo accounts is not possible at the moment") - @Nested - class LinkedNotificationBasedSignatureSession { - - @Test - void signing_withQrCode() { - DeviceLinkSessionResponse certificateChoiceSessionResponse = smartIdClient.createDeviceLinkCertificateRequest() - .withCertificateLevel(CertificateLevel.QUALIFIED) - .initCertificateChoice(); - - // Next steps: - // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse - // - Start querying sessions status - - // Use sessionID to start polling for session status - String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); - // Following values are used for generating device link or QR-code - String sessionToken = certificateChoiceSessionResponse.sessionToken(); - // Store sessionSecret only on backend side. Do not expose it to the client side. - String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); - URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); - // Will be used to calculate elapsed time being used in device link and in authCode - Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); - - // Calculate elapsed seconds from response received time - long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); - - // Build the device link URI - // This base URI will be used for QR code or App2App flows - URI deviceLink = smartIdClient.createDynamicContent() - .withDeviceLinkBase(deviceLinkBase.toString()) - .withDeviceLinkType(DeviceLinkType.QR_CODE) - .withSessionType(SessionType.CERTIFICATE_CHOICE) - .withSessionToken(sessionToken) - .withElapsedSeconds(elapsedSeconds) - .withLang("est") - .buildDeviceLink(sessionSecret); - - // Return URI to be used with QR-code generation library on the frontend side - // or create QR-code data-URI from device link and return that to the client side - String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); - - // Use sessionId to poll for certificate choice session status updates - SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); - SessionStatus certificateSessionStatus = poller.fetchFinalSessionStatus(certificateChoiceSessionId); - - // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. - assertEquals("COMPLETED", certificateSessionStatus.getState()); - - // Validate the certificate choice response - CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().build()); - CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); - CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); - - // For example construct DataToSign using digidoc4j library and queried certificate - // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); - - // Create the signable data from DataToSign - var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); - - // Start the linked notification signature session using the sessionID from the certificate choice session - LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() - .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) - .withLinkedSessionID(certificateChoiceSessionId) - .withSignableData(signableData) - .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign it!"))) - .initSignatureSession(); - - // Use sessionId to poll for signature session status updates - SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionResponse.sessionID()); - assertEquals("COMPLETED", signatureSessionStatus.getState()); - - // Validate signature response - SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); - SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); - - assertNotNull(signatureResponse.getSignatureValue()); - } - } - - private static KeyStore getKeystore() { - try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(is, "changeit".toCharArray()); - return keyStore; - } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { - throw new RuntimeException("Cannot find demo truststore", e); - } - } -} +package ee.sk.smartid.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.AuthenticationCertificateLevel; +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.CertificateByDocumentNumberResult; +import ee.sk.smartid.CertificateChoiceResponse; +import ee.sk.smartid.CertificateChoiceResponseValidator; +import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.CertificateValidator; +import ee.sk.smartid.CertificateValidatorImpl; +import ee.sk.smartid.DeviceLinkAuthenticationResponseValidator; +import ee.sk.smartid.DeviceLinkAuthenticationSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkSignatureSessionRequestBuilder; +import ee.sk.smartid.DeviceLinkType; +import ee.sk.smartid.FileTrustedCAStoreBuilder; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.NotificationAuthenticationResponseValidator; +import ee.sk.smartid.NotificationAuthenticationSessionRequestBuilder; +import ee.sk.smartid.QrCodeGenerator; +import ee.sk.smartid.RpChallenge; +import ee.sk.smartid.RpChallengeGenerator; +import ee.sk.smartid.SessionType; +import ee.sk.smartid.SignableData; +import ee.sk.smartid.SignatureCertificatePurposeValidator; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactory; +import ee.sk.smartid.SignatureCertificatePurposeValidatorFactoryImpl; +import ee.sk.smartid.SignatureResponse; +import ee.sk.smartid.SignatureResponseValidator; +import ee.sk.smartid.SignatureValueValidator; +import ee.sk.smartid.SignatureValueValidatorImpl; +import ee.sk.smartid.SmartIdClient; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.TrustedCACertStore; +import ee.sk.smartid.VerificationCodeCalculator; +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteraction; +import ee.sk.smartid.common.notification.interactions.NotificationInteraction; +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.util.CallbackUrlUtil; + +@SmartIdDemoIntegrationTest +public class ReadmeIntegrationTest { + + private static final Pattern NUMERIC_PATTERN = Pattern.compile("^[0-9]{4}$"); + + private SmartIdClient smartIdClient; + + @BeforeEach + void setUp() { + smartIdClient = new SmartIdClient(); + smartIdClient.setRelyingPartyUUID("00000000-0000-4000-8000-000000000000"); + smartIdClient.setRelyingPartyName("DEMO"); + smartIdClient.setHostUrl("https://sid.demo.sk.ee/smart-id-rp/v3/"); + + KeyStore keyStore = getKeystore(); + smartIdClient.setTrustStore(keyStore); + } + + @Disabled("Testing with device-link demo accounts is not possible at the moment") + @Nested + class DeviceLinkBasedExamples { + + @Nested + class Authentication { + + @Test + void anonymousAuthentication_withApp2App() { + // For security reasons a new hash value must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Create initial callback URL. + // Store the url-token only on backend side. Do not expose it to the client side. + // The url-token will be used to validate the callback request received from Smart-ID API + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + + // Setup builder + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + // to use anonymous authentication, do not set semantics identifier or document number + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )) + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()); + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link and in authCode + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.APP_2_APP) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withLang("est") + .withInitialCallbackUrl(callbackUrl.initialCallbackUri().toString()) + .withInteractions(authenticationSessionRequest.interactions()) + .buildDeviceLink(sessionSecret); + + // Use the sessionId from the authentication session response to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + // The session can have different states such as RUNNING or COMPLETE. + // Check that the session has completed successfully + assertEquals("COMPLETE", sessionStatus.getState()); + + // Receive callback from Smart-ID API + // Extract query parameters from the callback URL received + Map queryParameters = Map.of("value", callbackUrl.urlToken(), "sessionSecretDigest", "asdjlaksdjklf", "userChallengeVerifier", "abachdfajklsfa"); + + // Validate there is active user session in the application with matching url-token + String tokenInUrl = queryParameters.get("value"); + + // Validate that sessionSecretDigest in the callback URL validates against sessionSecret from the init session response + CallbackUrlUtil.validateSessionSecretDigest(queryParameters.get("sessionSecretDigest"), sessionSecret); + + // Set up AuthenticationResponseValidator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + DeviceLinkAuthenticationResponseValidator deviceLinkAuthenticationResponseValidator = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator); + // Validate the certificate and signature, then map the authentication response to the user's identity + AuthenticationIdentity authenticationIdentity = deviceLinkAuthenticationResponseValidator.validate( + sessionStatus, + builder.getAuthenticationSessionRequest(), + queryParameters.get("userChallengeVerifier"), + "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifierAndQrCode() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withSemanticsIdentifier(semanticsIdentifier) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + + // Get authentication session request used for starting the authentication session and use it later to validate sessions status response + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // Use sessionID to start polling for session status + String sessionId = authenticationSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + // Build the device link URI (without the authCode parameter) + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", sessionStatus.getState()); + + // Validate the response and return user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withDocumentNumberAndQrCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new rpChallenge must be created for each new authentication request + String rpChallenge = RpChallengeGenerator.generate().toBase64EncodedValue(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication session status OK response + + DeviceLinkAuthenticationSessionRequestBuilder builder = smartIdClient + .createDeviceLinkAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge) + .withInteractions(Collections.singletonList( + DeviceLinkInteraction.displayTextAndPin("Log in?") + )); + + // Init authentication session + DeviceLinkSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get AuthenticationSessionRequest after the request is made and store for validations + DeviceLinkAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + String sessionId = authenticationSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + String sessionToken = authenticationSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = authenticationSessionResponse.sessionSecret(); + Instant responseReceivedAt = authenticationSessionResponse.receivedAt(); + URI deviceLinkBase = authenticationSessionResponse.deviceLinkBase(); + + // Generate the base (unprotected) device link URI, which does not yet include the authCode + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.AUTHENTICATION) + .withSessionToken(sessionToken) + .withDigest(rpChallenge) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withInteractions(authenticationSessionRequest.interactions()) + .withLang("est") + .buildDeviceLink(sessionSecret); + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETE", sessionStatus.getState()); + + // Validate the certificate and signature, then map the authentication response to the user's identity + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + AuthenticationIdentity authenticationIdentity = DeviceLinkAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, null, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("EE", authenticationIdentity.getCountry()); + } + } + + @Nested + class Signature { + + @Test + void signature_withDocumentNumberAndQRCode() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + var deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QSCD) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withInteractions(signatureInteractions); + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); + URI deviceLinkBase = signatureSessionResponse.deviceLinkBase(); + + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withSchemeName("smart-id-demo") + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + TrustedCACertStore trustedCaCertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCaCertStore); + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + // Validate signature response + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + // Validate signature value + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), signableData.calculateHash(), certResponse.certificate(), signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticIdentifier) + .withCertificateLevel(CertificateLevel.QSCD) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Build the device link signature request + List signatureInteractions = List.of(DeviceLinkInteraction.displayTextAndPin("Please sign the document")); + DeviceLinkSignatureSessionRequestBuilder deviceLinkSignatureSessionRequestBuilder = smartIdClient.createDeviceLinkSignature() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(signatureInteractions); + + // Init signature session + DeviceLinkSessionResponse signatureSessionResponse = deviceLinkSignatureSessionRequestBuilder.initSignatureSession(); + // Get SignatureSessionRequest after the request is made and store for validations + DeviceLinkSignatureSessionRequest deviceLinkSignatureSessionRequest = deviceLinkSignatureSessionRequestBuilder.getSignatureSessionRequest(); + + // Process the signature response + String signatureSessionId = signatureSessionResponse.sessionID(); + String sessionToken = signatureSessionResponse.sessionToken(); + + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = signatureSessionResponse.sessionSecret(); + Instant receivedAt = signatureSessionResponse.receivedAt(); + + // Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the signatureSessionResponse + // Start querying sessions status + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(receivedAt, Instant.now()).getSeconds(); + // Generate auth code + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase("smartid://") + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.SIGNATURE) + .withSessionToken(sessionToken) + .withRelyingPartyName(Base64.getEncoder().encodeToString(smartIdClient.getRelyingPartyName().getBytes(StandardCharsets.UTF_8))) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .withInteractions(deviceLinkSignatureSessionRequest.interactions()) // interactions string must be the same as in the signature session request + .buildDeviceLink(sessionSecret); + // Display QR-code to the user + + // Get the session status poller + poller = smartIdClient.getSessionStatusPoller(); + // Get signatureSessionId from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + // Validate signature value + SignatureValueValidator signatureValueValidator = new SignatureValueValidatorImpl(); + signatureValueValidator.validate(signatureResponse.getSignatureValue(), + signableData.calculateHash(), + certificateChoiceResponse.getCertificate(), + signatureResponse.getRsaSsaPssParameters()); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getRequestedCertificateLevel()); + assertEquals("displayTextAndPIN", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + } + + @Nested + class NotificationBasedExamples { + + @Test + void authentication_withDocumentNumber() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // For security reasons a new rpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient + .createNotificationAuthentication() + .withDocumentNumber(documentNumber) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))); + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // SessionID is used to query sessions status later + String sessionId = authenticationSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Use sessionID from current session response to poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals(documentNumber, sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + + // validate the sessions status and return user's identity + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void authentication_withSemanticIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // For security reasons a new RpChallenge must be created for each new authentication request + RpChallenge rpChallenge = RpChallengeGenerator.generate(); + // Store generated rpChallenge only on backend side. Do not expose it to the client side. + // Used for validating authentication sessions status OK response + + // Generate verification code to be displayed to the user + String verificationCode = VerificationCodeCalculator.calculate(rpChallenge.value()); + + NotificationAuthenticationSessionRequestBuilder builder = smartIdClient.createNotificationAuthentication() + .withSemanticsIdentifier(semanticIdentifier) + .withRpChallenge(rpChallenge.toBase64EncodedValue()) + .withCertificateLevel(AuthenticationCertificateLevel.QUALIFIED) + .withInteractions(Collections.singletonList( + NotificationInteraction.displayTextAndPin("Log in?"))); + + // Init authentication session + NotificationAuthenticationSessionResponse authenticationSessionResponse = builder.initAuthenticationSession(); + // Get notification-based authentication session request used for starting the authentication session + // and use it later to validate sessions status response + NotificationAuthenticationSessionRequest authenticationSessionRequest = builder.getAuthenticationSessionRequest(); + + // SessionID is used to query sessions status later + String sessionId = authenticationSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + // Use sessionID from current session response to poll for session status + SessionStatus sessionStatus = poller.fetchFinalSessionStatus(sessionId); + + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PNOLT-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + AuthenticationIdentity authenticationIdentity = + NotificationAuthenticationResponseValidator.defaultSetupWithCertificateValidator(certificateValidator) + .validate(sessionStatus, authenticationSessionRequest, "smart-id-demo"); + + assertEquals("40504040001", authenticationIdentity.getIdentityCode()); + assertEquals("OK", authenticationIdentity.getGivenName()); + assertEquals("TESTNUMBER", authenticationIdentity.getSurname()); + assertEquals("LT", authenticationIdentity.getCountry()); + } + + @Test + void certificateChoice_withSemanticIdentifier() { + var semanticsIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.LT, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + // Use requested certificate level to validate certificate choice session status OK response. + CertificateLevel requestedCertificateLevel = CertificateLevel.QSCD; // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticsIdentifier) + .withCertificateLevel(requestedCertificateLevel) + .initCertificateChoice(); + + String sessionId = certificateChoiceSessionResponse.sessionID(); + // SessionID is used to query sessions status later + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus sessionStatus = poller.getSessionStatus(sessionId); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(sessionStatus, requestedCertificateLevel); + + assertEquals("OK", response.getEndResult()); + assertEquals("PNOLT-40504040001-MOCK-Q", response.getDocumentNumber()); + assertNotNull(response.getCertificate()); + assertEquals(CertificateLevel.QUALIFIED, response.getCertificateLevel()); + } + + @Test + void signature_withSemanticsIdentifier() { + var semanticIdentifier = new SemanticsIdentifier( + // 3 character identity type + // (PAS-passport, IDC-national identity card or PNO - (national) personal number) + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, // 2 character ISO 3166-1 alpha-2 country code + "40504040001"); // identifier (according to country and identity type reference) + + CertificateLevel certificateLevel = CertificateLevel.QSCD; + NotificationCertificateChoiceSessionResponse certificateChoiceSessionResponse = smartIdClient + .createNotificationCertificateChoice() + .withSemanticsIdentifier(semanticIdentifier) + .withCertificateLevel(certificateLevel) // Certificate level can either be "QUALIFIED", "ADVANCED" or "QSCD" + .initCertificateChoice(); + + // SessionID is used to query sessions status later + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Querying the sessions status + SessionStatus certificateSessionStatus = poller.getSessionStatus(certificateChoiceSessionId); + + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse response = certificateChoiceResponseValidator.validate(certificateSessionStatus, certificateLevel); + // For example use digidoc4j use SignatureBuilder to create DataToSign using certificateChoiceResponse.getCertificate(); + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + // Create the Semantics Identifier + var semanticsIdentifier = new SemanticsIdentifier( + SemanticsIdentifier.IdentityType.PNO, + SemanticsIdentifier.CountryCode.EE, + "40504040001" + ); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(certificateLevel) + .withSignableData(signableData) + .withSemanticsIdentifier(semanticsIdentifier) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the document")) + ) + .initSignatureSession(); + + // Get the session ID and continue to querying session status + String sessionID = signatureSessionResponse.sessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(sessionID); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals("PNOEE-40504040001-DEMO-Q", signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + + @Test + void signature_withDocumentNumber() { + String documentNumber = "PNOEE-40504040001-DEMO-Q"; + + CertificateLevel certificateLevel = CertificateLevel.QSCD; + // Query the certificate by document number to be used for creating the DataToSign + CertificateByDocumentNumberResult certificateByDocumentNumber = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .withCertificateLevel(certificateLevel) + .getCertificateByDocumentNumber(); + + // Set up the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + // Validate the certificate is trusted and active + certificateValidator.validate(certificateByDocumentNumber.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certificateByDocumentNumber.certificateLevel()); + certificatePurposeValidator.validate(certificateByDocumentNumber.certificate()); + + // For example use digidoc4j with SignatureBuilder to create DataToSign using `certificateByDocumentNumber.certificate()` + + // Create the signable data + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_512); + + NotificationSignatureSessionResponse signatureSessionResponse = smartIdClient.createNotificationSignature() + .withCertificateLevel(certificateLevel) + .withSignableData(signableData) + .withDocumentNumber(documentNumber) + .withInteractions(List.of( + NotificationInteraction.confirmationMessage("Please sign the document")) + ) + .initSignatureSession(); + + // Get the session ID and continue to querying session status + String signatureSessionId = signatureSessionResponse.sessionID(); + + // Display verification code to the user + String verificationCode = signatureSessionResponse.vc().value(); + assertTrue(NUMERIC_PATTERN.matcher(verificationCode).matches()); + + // Get the session status poller + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + + // Get sessionID from current session response and poll for session status + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionId); + // Session can have two states RUNNING or COMPLETED, check sessionStatus.getResult().getEndResult() for OK or error responses (f.e USER_REFUSED, TIMEOUT) + assertEquals("COMPLETE", signatureSessionStatus.getState()); + + SignatureResponseValidator validator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = validator.validate(signatureSessionStatus, certificateLevel); + + assertEquals("OK", signatureResponse.getEndResult()); + assertEquals(documentNumber, signatureResponse.getDocumentNumber()); + assertEquals(CertificateLevel.QUALIFIED, signatureResponse.getCertificateLevel()); + assertEquals(CertificateLevel.QSCD, signatureResponse.getRequestedCertificateLevel()); + assertEquals("confirmationMessage", signatureResponse.getInteractionFlowUsed()); + assertNotNull(signatureResponse.getCertificate()); + } + } + + @Nested + class CertificateByDocumentNumberExamples { + + @Test + void queryCertificate() { + String documentNumber = "PNOLT-40504040001-MOCK-Q"; + + // Build the certificate by document number request and query the certificate + CertificateByDocumentNumberResult certResponse = smartIdClient + .createCertificateByDocumentNumber() + .withDocumentNumber(documentNumber) + .getCertificateByDocumentNumber(); + + // Set up the certificate validator + TrustedCACertStore trustedCACertStore = new FileTrustedCAStoreBuilder().build(); + CertificateValidator certificateValidator = new CertificateValidatorImpl(trustedCACertStore); + + // Validate the certificate + certificateValidator.validate(certResponse.certificate()); + + // Validate the certificate is suitable for signing + SignatureCertificatePurposeValidatorFactory signatureCertificatePurposeValidatorFactory = new SignatureCertificatePurposeValidatorFactoryImpl(); + SignatureCertificatePurposeValidator certificatePurposeValidator = signatureCertificatePurposeValidatorFactory.create(certResponse.certificateLevel()); + certificatePurposeValidator.validate(certResponse.certificate()); + } + } + + @Disabled("Testing with device-link demo accounts is not possible at the moment") + @Nested + class LinkedNotificationBasedSignatureSession { + + @Test + void signing_withQrCode() { + DeviceLinkSessionResponse certificateChoiceSessionResponse = smartIdClient.createDeviceLinkCertificateRequest() + .withCertificateLevel(CertificateLevel.QUALIFIED) + .initCertificateChoice(); + + // Next steps: + // - Generate QR-code or device link to be displayed to the user using sessionToken, sessionSecret and receivedAt provided in the authenticationResponse + // - Start querying sessions status + + // Use sessionID to start polling for session status + String certificateChoiceSessionId = certificateChoiceSessionResponse.sessionID(); + // Following values are used for generating device link or QR-code + String sessionToken = certificateChoiceSessionResponse.sessionToken(); + // Store sessionSecret only on backend side. Do not expose it to the client side. + String sessionSecret = certificateChoiceSessionResponse.sessionSecret(); + URI deviceLinkBase = certificateChoiceSessionResponse.deviceLinkBase(); + // Will be used to calculate elapsed time being used in device link and in authCode + Instant responseReceivedAt = certificateChoiceSessionResponse.receivedAt(); + + // Calculate elapsed seconds from response received time + long elapsedSeconds = Duration.between(responseReceivedAt, Instant.now()).getSeconds(); + + // Build the device link URI + // This base URI will be used for QR code or App2App flows + URI deviceLink = smartIdClient.createDynamicContent() + .withDeviceLinkBase(deviceLinkBase.toString()) + .withDeviceLinkType(DeviceLinkType.QR_CODE) + .withSessionType(SessionType.CERTIFICATE_CHOICE) + .withSessionToken(sessionToken) + .withElapsedSeconds(elapsedSeconds) + .withLang("est") + .buildDeviceLink(sessionSecret); + + // Return URI to be used with QR-code generation library on the frontend side + // or create QR-code data-URI from device link and return that to the client side + String dataUri = QrCodeGenerator.generateDataUri(deviceLink.toString()); + + // Use sessionId to poll for certificate choice session status updates + SessionStatusPoller poller = smartIdClient.getSessionStatusPoller(); + SessionStatus certificateSessionStatus = poller.fetchFinalSessionStatus(certificateChoiceSessionId); + + // The session can have states such as RUNNING or COMPLETE. Check that the session has completed successfully. + assertEquals("COMPLETED", certificateSessionStatus.getState()); + + // Validate the certificate choice response + CertificateValidatorImpl certificateValidator = new CertificateValidatorImpl(new FileTrustedCAStoreBuilder().build()); + CertificateChoiceResponseValidator certificateChoiceResponseValidator = new CertificateChoiceResponseValidator(certificateValidator); + CertificateChoiceResponse certificateChoiceResponse = certificateChoiceResponseValidator.validate(certificateSessionStatus); + + // For example construct DataToSign using digidoc4j library and queried certificate + // DataToSign dataToSign = toDataToSign(container,certResponse.certificate()); + + // Create the signable data from DataToSign + var signableData = new SignableData("dataToSign".getBytes(), HashAlgorithm.SHA_256); + + // Start the linked notification signature session using the sessionID from the certificate choice session + LinkedSignatureSessionResponse signatureSessionResponse = smartIdClient.createLinkedNotificationSignature() + .withDocumentNumber(certificateChoiceResponse.getDocumentNumber()) + .withLinkedSessionID(certificateChoiceSessionId) + .withSignableData(signableData) + .withInteractions(List.of(DeviceLinkInteraction.displayTextAndPin("Sign it!"))) + .initSignatureSession(); + + // Use sessionId to poll for signature session status updates + SessionStatus signatureSessionStatus = poller.fetchFinalSessionStatus(signatureSessionResponse.sessionID()); + assertEquals("COMPLETED", signatureSessionStatus.getState()); + + // Validate signature response + SignatureResponseValidator signatureResponseValidator = new SignatureResponseValidator(certificateValidator); + SignatureResponse signatureResponse = signatureResponseValidator.validate(signatureSessionStatus, CertificateLevel.QUALIFIED); + + assertNotNull(signatureResponse.getSignatureValue()); + } + } + + private static KeyStore getKeystore() { + try (InputStream is = ReadmeIntegrationTest.class.getResourceAsStream("/demo_server_trusted_ssl_certs.jks")) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, "changeit".toCharArray()); + return keyStore; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot find demo truststore", e); + } + } +} diff --git a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java index 21698195..69ba46f4 100644 --- a/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java +++ b/src/test/java/ee/sk/smartid/integration/SmartIdRestIntegrationTest.java @@ -1,332 +1,332 @@ -package ee.sk.smartid.integration; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; -import java.util.regex.Pattern; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.DigestCalculator; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.RpChallengeGenerator; -import ee.sk.smartid.SignatureAlgorithm; -import ee.sk.smartid.SignatureProtocol; -import ee.sk.smartid.SmartIdDemoIntegrationTest; -import ee.sk.smartid.VerificationCodeType; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.SmartIdRestConnector; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.util.InteractionUtil; - -@SmartIdDemoIntegrationTest -class SmartIdRestIntegrationTest { - - private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; - private static final String RELYING_PARTY_NAME = "DEMO"; - - private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); - private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9]{4}$"); - private static final Pattern SESSION_TOKEN_PATTERN = Pattern.compile("^[A-Za-z0-9]{24}$"); - private static final Pattern SESSION_SECRET_PATTERN = Pattern.compile("^[A-Za-z0-9+/]{24}$"); - - private SmartIdConnector smartIdConnector; - - @BeforeEach - void setUp() { - smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); - } - - @Disabled("Testing device-link flows with demo accounts is not yet possible") - @Nested - class DeviceLink { - - @Nested - class Authentication { - - @Test - void initAnonymousDeviceLinkAuthentication() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkAuthentication_withDocumentNumber() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkAuthentication_withSemanticsIdentifier() { - DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { - var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate().toBase64EncodedValue(), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new DeviceLinkAuthenticationSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.ACSP_V2, - signatureParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - null, - null, - null); - } - } - - @Nested - class CertificateChoice { - - @Test - void initDeviceLinkCertificateChoice() { - var request = new DeviceLinkCertificateChoiceSessionRequest( - RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - null, - null, - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.deviceLinkBase()); - assertNotNull(sessionResponse.receivedAt()); - } - } - - @Nested - class Signature { - - @Test - void initDeviceLinkSignature_withSemanticIdentifier() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - - @Test - void initDeviceLinkSignature_withDocumentNumber() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null, - null - ); - - DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); - assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); - assertNotNull(sessionResponse.receivedAt()); - } - } - } - - @Nested - class NotificationBasedRequests { - - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-40504040001"); - private static final String DOCUMENT_NUMBER = "PNOEE-40504040001-DEMO-Q"; - - @Nested - class Authentication { - - @Test - void initNotificationAuthentication_withSemanticIdentifier() { - var request = toAuthenticationRequest(); - - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - - @Test - void initNotificationAuthentication_withDocumentNumber() { - var request = toAuthenticationRequest(); - - NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, DOCUMENT_NUMBER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - - private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { - var signatureParameters = new AcspV2SignatureProtocolParameters( - RpChallengeGenerator.generate().toBase64EncodedValue(), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.ACSP_V2.name(), - signatureParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - new RequestProperties(true), - null, - VerificationCodeType.NUMERIC4.getValue() - ); - } - } - - @Nested - class CertificateChoice { - - @Test - void initNotificationCertificateChoice_withSemanticIdentifier() { - var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); - - NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - } - } - - @Nested - class Signature { - - @Test - void initNotificationSignature_withSemanticIdentifier() { - var request = toSignatureSessionRequest(); - - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); - assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); - } - - @Test - void initNotificationCertificateChoice_withDocumentNumber() { - var request = toSignatureSessionRequest(); - - NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); - assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); - assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); - } - - private static NotificationSignatureSessionRequest toSignatureSessionRequest() { - var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( - Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), - SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new NotificationSignatureSessionRequest(RELYING_PARTY_UUID, - RELYING_PARTY_NAME, - "QUALIFIED", - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - signatureProtocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), - null - ); - } - } - } -} +package ee.sk.smartid.integration; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.regex.Pattern; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.DigestCalculator; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.RpChallengeGenerator; +import ee.sk.smartid.SignatureAlgorithm; +import ee.sk.smartid.SignatureProtocol; +import ee.sk.smartid.SmartIdDemoIntegrationTest; +import ee.sk.smartid.VerificationCodeType; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.SmartIdRestConnector; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.util.InteractionUtil; + +@SmartIdDemoIntegrationTest +class SmartIdRestIntegrationTest { + + private static final String RELYING_PARTY_UUID = "00000000-0000-4000-8000-000000000000"; + private static final String RELYING_PARTY_NAME = "DEMO"; + + private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + private static final Pattern VERIFICATION_CODE_PATTERN = Pattern.compile("^[A-Za-z0-9]{4}$"); + private static final Pattern SESSION_TOKEN_PATTERN = Pattern.compile("^[A-Za-z0-9]{24}$"); + private static final Pattern SESSION_SECRET_PATTERN = Pattern.compile("^[A-Za-z0-9+/]{24}$"); + + private SmartIdConnector smartIdConnector; + + @BeforeEach + void setUp() { + smartIdConnector = new SmartIdRestConnector("https://sid.demo.sk.ee/smart-id-rp/v3/"); + } + + @Disabled("Testing device-link flows with demo accounts is not yet possible") + @Nested + class DeviceLink { + + @Nested + class Authentication { + + @Test + void initAnonymousDeviceLinkAuthentication() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initAnonymousDeviceLinkAuthentication(request); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkAuthentication_withDocumentNumber() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkAuthentication_withSemanticsIdentifier() { + DeviceLinkAuthenticationSessionRequest request = toDeviceLinkAuthenticationSessionRequest(); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkAuthentication(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest() { + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate().toBase64EncodedValue(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new DeviceLinkAuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2, + signatureParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + null, + null, + null); + } + } + + @Nested + class CertificateChoice { + + @Test + void initDeviceLinkCertificateChoice() { + var request = new DeviceLinkCertificateChoiceSessionRequest( + RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + null, + null, + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkCertificateChoice(request); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.deviceLinkBase()); + assertNotNull(sessionResponse.receivedAt()); + } + } + + @Nested + class Signature { + + @Test + void initDeviceLinkSignature_withSemanticIdentifier() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters(Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA3_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, new SemanticsIdentifier("PNOEE-40504040001")); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + + @Test + void initDeviceLinkSignature_withDocumentNumber() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + var request = new DeviceLinkSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null, + null + ); + + DeviceLinkSessionResponse sessionResponse = smartIdConnector.initDeviceLinkSignature(request, "PNOEE-40504040001-MOCK-Q"); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(SESSION_TOKEN_PATTERN.matcher(sessionResponse.sessionToken()).matches()); + assertTrue(SESSION_SECRET_PATTERN.matcher(sessionResponse.sessionSecret()).matches()); + assertNotNull(sessionResponse.receivedAt()); + } + } + } + + @Nested + class NotificationBasedRequests { + + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-40504040001"); + private static final String DOCUMENT_NUMBER = "PNOEE-40504040001-DEMO-Q"; + + @Nested + class Authentication { + + @Test + void initNotificationAuthentication_withSemanticIdentifier() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + + @Test + void initNotificationAuthentication_withDocumentNumber() { + var request = toAuthenticationRequest(); + + NotificationAuthenticationSessionResponse sessionResponse = smartIdConnector.initNotificationAuthentication(request, DOCUMENT_NUMBER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + + private static NotificationAuthenticationSessionRequest toAuthenticationRequest() { + var signatureParameters = new AcspV2SignatureProtocolParameters( + RpChallengeGenerator.generate().toBase64EncodedValue(), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.ACSP_V2.name(), + signatureParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + new RequestProperties(true), + null, + VerificationCodeType.NUMERIC4.getValue() + ); + } + } + + @Nested + class CertificateChoice { + + @Test + void initNotificationCertificateChoice_withSemanticIdentifier() { + var request = new NotificationCertificateChoiceSessionRequest(RELYING_PARTY_UUID, RELYING_PARTY_NAME, null, null, null, null); + + NotificationCertificateChoiceSessionResponse sessionResponse = smartIdConnector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + } + } + + @Nested + class Signature { + + @Test + void initNotificationSignature_withSemanticIdentifier() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); + } + + @Test + void initNotificationCertificateChoice_withDocumentNumber() { + var request = toSignatureSessionRequest(); + + NotificationSignatureSessionResponse sessionResponse = smartIdConnector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertTrue(UUID_PATTERN.matcher(sessionResponse.sessionID()).matches()); + assertTrue(VERIFICATION_CODE_PATTERN.matcher(sessionResponse.vc().value()).matches()); + assertEquals(VerificationCodeType.NUMERIC4.getValue(), sessionResponse.vc().type()); + } + + private static NotificationSignatureSessionRequest toSignatureSessionRequest() { + var signatureProtocolParameters = new RawDigestSignatureProtocolParameters( + Base64.toBase64String(DigestCalculator.calculateDigest("test".getBytes(), HashAlgorithm.SHA_512)), + SignatureAlgorithm.RSASSA_PSS.getAlgorithmName(), + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + return new NotificationSignatureSessionRequest(RELYING_PARTY_UUID, + RELYING_PARTY_NAME, + "QUALIFIED", + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + signatureProtocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign it!", null))), + null + ); + } + } + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java index 6f4970d1..b50ab0a9 100644 --- a/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java +++ b/src/test/java/ee/sk/smartid/rest/SessionStatusPollerTest.java @@ -1,83 +1,83 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import ee.sk.smartid.rest.SessionStatusPoller; -import ee.sk.smartid.rest.SmartIdConnector; -import ee.sk.smartid.rest.dao.SessionStatus; - -class SessionStatusPollerTest { - - private SmartIdConnector smartIdConnector; - - private SessionStatusPoller poller; - - @BeforeEach - void setUp() { - smartIdConnector = mock(SmartIdConnector.class); - poller = new SessionStatusPoller(smartIdConnector); - } - - @Test - void fetchFinalSessionStatus() { - SessionStatus runningStatus = new SessionStatus(); - runningStatus.setState("RUNNING"); - - SessionStatus completedStatus = new SessionStatus(); - completedStatus.setState("COMPLETE"); - - when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")) - .thenReturn(runningStatus, completedStatus); - - SessionStatus finalSessionStatus = poller.fetchFinalSessionStatus("00000000-0000-0000-0000-000000000000"); - - verify(smartIdConnector, times(2)).getSessionStatus("00000000-0000-0000-0000-000000000000"); - assertEquals("COMPLETE", finalSessionStatus.getState()); - } - - @Test - void getSessionStatus() { - SessionStatus sessionStatus = new SessionStatus(); - sessionStatus.setState("RUNNING"); - when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); - - SessionStatus sessionsStatus = poller.getSessionStatus("00000000-0000-0000-0000-000000000000"); - - assertEquals("RUNNING", sessionsStatus.getState()); - assertNull(sessionsStatus.getResult()); - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import ee.sk.smartid.rest.SessionStatusPoller; +import ee.sk.smartid.rest.SmartIdConnector; +import ee.sk.smartid.rest.dao.SessionStatus; + +class SessionStatusPollerTest { + + private SmartIdConnector smartIdConnector; + + private SessionStatusPoller poller; + + @BeforeEach + void setUp() { + smartIdConnector = mock(SmartIdConnector.class); + poller = new SessionStatusPoller(smartIdConnector); + } + + @Test + void fetchFinalSessionStatus() { + SessionStatus runningStatus = new SessionStatus(); + runningStatus.setState("RUNNING"); + + SessionStatus completedStatus = new SessionStatus(); + completedStatus.setState("COMPLETE"); + + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")) + .thenReturn(runningStatus, completedStatus); + + SessionStatus finalSessionStatus = poller.fetchFinalSessionStatus("00000000-0000-0000-0000-000000000000"); + + verify(smartIdConnector, times(2)).getSessionStatus("00000000-0000-0000-0000-000000000000"); + assertEquals("COMPLETE", finalSessionStatus.getState()); + } + + @Test + void getSessionStatus() { + SessionStatus sessionStatus = new SessionStatus(); + sessionStatus.setState("RUNNING"); + when(smartIdConnector.getSessionStatus("00000000-0000-0000-0000-000000000000")).thenReturn(sessionStatus); + + SessionStatus sessionsStatus = poller.getSessionStatus("00000000-0000-0000-0000-000000000000"); + + assertEquals("RUNNING", sessionsStatus.getState()); + assertNull(sessionsStatus.getResult()); + } +} diff --git a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java index 8e5d7ce0..0082e0ee 100644 --- a/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java +++ b/src/test/java/ee/sk/smartid/rest/SmartIdRestConnectorTest.java @@ -1,1779 +1,1779 @@ -package ee.sk.smartid.rest; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static com.github.tomakehurst.wiremock.client.WireMock.containing; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; -import static ee.sk.smartid.SmartIdRestServiceStubs.stubStrictRequestWithResponse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.StringStartsWith.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; - -import org.bouncycastle.util.encoders.Base64; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import ee.sk.smartid.CertificateLevel; -import ee.sk.smartid.HashAlgorithm; -import ee.sk.smartid.SignatureProtocol; -import ee.sk.smartid.SmartIdRestServiceStubs; -import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; -import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; -import ee.sk.smartid.exception.SessionNotFoundException; -import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; -import ee.sk.smartid.exception.permanent.ServerMaintenanceException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; -import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; -import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; -import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; -import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; -import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; -import ee.sk.smartid.rest.dao.CertificateResponse; -import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; -import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; -import ee.sk.smartid.rest.dao.Interaction; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; -import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; -import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; -import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; -import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; -import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; -import ee.sk.smartid.rest.dao.RequestProperties; -import ee.sk.smartid.rest.dao.SemanticsIdentifier; -import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionSignature; -import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.SessionStatus; -import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; -import ee.sk.smartid.rest.dao.VerificationCode; -import ee.sk.smartid.util.InteractionUtil; - -class SmartIdRestConnectorTest { - - private static final String SESSION_SECRET = "c2Vzc2lvblNlY3JldA=="; - - @Nested - @WireMockTest(httpPort = 18089) - class SessionStatusTests { - - private static final String SERVER_RANDOM = "J0iyCYOu8cTWuoD8rD05IIrZ"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void getSessionStatus_running() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - } - - @Test - void getSessionStatus_running_withIgnoredProperties() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running-with-ignored-properties.json"); - assertNotNull(sessionStatus); - assertEquals("RUNNING", sessionStatus.getState()); - assertNotNull(sessionStatus.getIgnoredProperties()); - assertEquals(2, sessionStatus.getIgnoredProperties().length); - assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); - assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); - } - - @Test - void getSessionStatus_forSuccessfulAuthenticationRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); - assertSuccessfulResponse(sessionStatus); - assertEquals("displayTextAndPIN", sessionStatus.getInteractionTypeUsed()); - - assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); - SessionSignature sessionSignature = sessionStatus.getSignature(); - assertNotNull(sessionSignature); - assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionSignature.getValue())); - assertEquals(SERVER_RANDOM, sessionSignature.getServerRandom()); - assertTrue(Pattern.matches("^[a-zA-Z0-9-_]{43}$", sessionSignature.getUserChallenge())); - assertEquals("QR", sessionSignature.getFlowType()); - assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); - - assertSignatureAlgorithmParameters(sessionSignature, "SHA3-512"); - - assertNotNull(sessionStatus.getCert()); - assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_forSuccessfulCertificateRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-certificate-choice.json"); - assertSuccessfulResponse(sessionStatus); - - assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_forSuccessfulSignatureRequest() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); - assertSuccessfulResponse(sessionStatus); - assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); - - assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); - SessionSignature sessionSignature = sessionStatus.getSignature(); - assertNotNull(sessionSignature); - assertThat(sessionSignature.getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); - assertEquals("QR", sessionSignature.getFlowType()); - assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); - - assertSignatureAlgorithmParameters(sessionSignature, "SHA-512"); - - assertNotNull(sessionStatus.getCert()); - assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); - assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); - } - - @Test - void getSessionStatus_hasUserAgentHeader() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); - assertSuccessfulResponse(sessionStatus); - - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) - .withHeader("User-Agent", containing("smart-id-java-client/")) - .withHeader("User-Agent", containing("Java/"))); - } - - @Test - void getSessionStatus_withTimeoutParameter() { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/session-status-successful-authentication.json"); - connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); - SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - assertSuccessfulResponse(sessionStatus); - verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); - } - - @Test - void getSessionStatus_whenSessionNotFound() { - assertThrows(SessionNotFoundException.class, () -> { - stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); - connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - }); - } - - @Nested - class UserRefusedInteractions { - - @Test - void getSessionStatus_userHasRefused() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessage() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("confirmationMessage", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("confirmationMessageAndVerificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedDisplayTextAndPin() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("displayTextAndPIN", sessionStatus.getResult().getDetails().getInteraction()); - } - - @Test - void getSessionStatus_userHasRefusedVerificationCodeChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); - assertEquals("verificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); - } - } - - @Test - void getSessionStatus_userHasRefusedCertChoice() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("USER_REFUSED_CERT_CHOICE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_timeout() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("TIMEOUT", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_userHasSelectedWrongVcCode() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("WRONG_VC", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_documentUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("DOCUMENT_UNUSABLE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_protocolFailure() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-protocol-failure.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("PROTOCOL_FAILURE", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_expectedLinkedSession() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-expected-linked-session.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("EXPECTED_LINKED_SESSION", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_serverError() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-server-error.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); - } - - @Test - void getSessionStatus_accountUnusable() { - SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-account-unusable.json"); - assertEquals("COMPLETE", sessionStatus.getState()); - assertEquals("ACCOUNT_UNUSABLE", sessionStatus.getResult().getEndResult()); - } - - private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { - stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); - return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); - } - - private static void assertSuccessfulResponse(SessionStatus sessionStatus) { - assertEquals("COMPLETE", sessionStatus.getState()); - assertNotNull(sessionStatus.getResult()); - assertEquals("OK", sessionStatus.getResult().getEndResult()); - assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); - } - - private static void assertSignatureAlgorithmParameters(SessionSignature sessionSignature, String expectedHashAlgorithm) { - SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); - assertEquals(expectedHashAlgorithm, signatureAlgorithmParameters.getHashAlgorithm()); - var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); - assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); - SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); - assertEquals(expectedHashAlgorithm, parameters.getHashAlgorithm()); - assertEquals(64, signatureAlgorithmParameters.getSaltLength()); - assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); - } - } - - @Nested - @WireMockTest(httpPort = 18082) - class SemanticsIdentifierDeviceLinkAuthentication { - - private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-30303039914"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18082"); - } - - @Test - void initDeviceLinkAuthentication_qrCodeFlow_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toQrAuthenticationSessionRequest(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_sameDeviceOnlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(null, "https://example.com/callback"); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_sameDeviceAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(new RequestProperties(true), "https://example.com/callback"); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_accountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, () -> - connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18083) - class DocumentNumberDeviceLinkAuthentication { - - private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18083"); - } - - @Test - void initDeviceLinkAuthentication() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), "PNOEE-48010010101-MOCK-Q")); - } - - @Test - void initDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - } - - @Nested - @WireMockTest(httpPort = 18081) - class AnonymousDeviceLinkAuthentication { - - private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/device-link/anonymous"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18081"); - } - - @Test - void initAnonymousDeviceLinkAuthentication_qrCodeFlow_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initAnonymousDeviceLinkAuthentication_sameDeviceFlow_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, - "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", - "responses/auth/device-link/device-link-authentication-session-response.json"); - - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); - Instant end = Instant.now(); - - assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); - } - - @Test - void initAnonymousDeviceLinkAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - - @Test - void initAnonymousDeviceLinkAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); - } - } - - @Nested - @WireMockTest(httpPort = 18082) - class SemanticsIdentifierNotificationAuthentication { - - private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18082"); - } - - @Test - void initNotificationAuthentication_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, - "requests/auth/notification/notification-authentication-session-request-all-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), SEMANTICS_IDENTIFIER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); - - assertThrows(SmartIdClientException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18083) - class DocumentNumberNotificationAuthentication { - - private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18083"); - } - - @Test - void initNotificationAuthentication_onlyRequeriedFields_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_allFields_ok() { - SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, - "requests/auth/notification/notification-authentication-session-request-all-fields.json", - "responses/auth/notification/notification-session-response.json"); - - NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( - toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), DOCUMENT_NUMBER); - - assertNotNull(response); - } - - @Test - void initNotificationAuthentication_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); - - var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(authenticationRequest, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> - connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_forbiddenForRP_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - - @Test - void initNotificationAuthentication_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkCertificateChoiceTests { - - private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/signature/certificate-choice/device-link/anonymous"; - - private SmartIdConnector connector; - - @BeforeEach - public void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initDeviceLinkCertificateChoice() { - stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - Instant start = Instant.now(); - DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); - Instant end = Instant.now(); - - assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); - } - - @Test - void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_userAccountNotFound() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_invalidRequest() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); - - var request = new DeviceLinkCertificateChoiceSessionRequest("", "", null, null, null, null, null); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); - } - - @Test - void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - @Test - void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); - } - - @Test - void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { - stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); - - DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); - } - - private static DeviceLinkCertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { - return new DeviceLinkCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - "ADVANCED", - null, - null, - null, - null - ); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class LinkedNotificationSignature { - - private static final String LINKED_SIGNATURE_PATH = "/signature/notification/linked/PNOEE-31111111111-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-31111111111-MOCK-Q"; - private static final String NONCE = "cmFuZG9tTm9uY2U="; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initLinkedNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionRequest request = toLinkedSignatureSessionRequest(null, null, null); - LinkedSignatureSessionResponse linkedSignatureSessionResponse = connector.initLinkedNotificationSignature(request, DOCUMENT_NUMBER); - - assertNotNull(linkedSignatureSessionResponse); - assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); - } - - @Test - void initLinkedNotificationSignature_withAllFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); - - LinkedSignatureSessionResponse linkedSignatureSessionResponse = - connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER); - - assertNotNull(linkedSignatureSessionResponse); - assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); - } - - @Test - void initLinkedNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(SmartIdClientException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_accountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); - - assertThrows(UserAccountNotFoundException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_issueWithUserAccount_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 472); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { - var linkedSignatureSessionRequest = toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, "cmFuZG9tTm9uY2U=", new RequestProperties(true)); - connector.initLinkedNotificationSignature(linkedSignatureSessionRequest, DOCUMENT_NUMBER); - }); - } - - @Test - void initLinkedNotificationSignature_apiClientBeingUsedIsOutdated_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); - - assertThrows(SmartIdClientException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - @Test - void initLinkedNotificationSignature_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 580); - - assertThrows(ServerMaintenanceException.class, - () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); - } - - private LinkedSignatureSessionRequest toFullLinkedSignatureSessionRequest() { - return toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, NONCE, new RequestProperties(true)); - } - - private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(CertificateLevel certificateLevel, - String nonce, - RequestProperties requestProperties) { - var rawDigestSignatureProtocolParameters = new RawDigestSignatureProtocolParameters( - "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); - return new LinkedSignatureSessionRequest("00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel != null ? certificateLevel.name() : null, - "RAW_DIGEST_SIGNATURE", - rawDigestSignatureProtocolParameters, - "10000000-0000-000-000-000000000000", - nonce, - "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", - requestProperties, - null); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class SemanticsIdentifierNotificationCertificateChoiceTests { - - private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/signature/certificate-choice/notification/etsi/PNOEE-31111111111"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); - - private SmartIdRestConnector connector; - - @BeforeEach - public void setUp() { - WireMock.configureFor("localhost", 18089); - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initCertificateChoice_onlyRequiredFields_successful() { - stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); - - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @Test - void initCertificateChoice_allFields_successful() { - stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", - "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); - var request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - "QUALIFIED", - "cmFuZG9tTm9uY2U=", - null, - new RequestProperties(true)); - SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); - - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - } - - @Test - void initCertificateChoice_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - null, - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_rpDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - null, - null, - null, - null); - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_suitableAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 471); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_userShouldCheckPortal_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 472); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_javaClientBeingUsedIsTooOld_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 480); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initCertificateChoice_systemUnderMaintenance_throwException() { - SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 580); - - NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( - "00000000-0000-4000-8000-000000000000", - "NOT DEMO", - null, - null, - null, - null); - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18086) - class CertificateByDocumentNumberTests { - - private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18086"); - } - - @Test - void getCertificateByDocumentNumber_successful() { - SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); - - CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest()); - - assertNotNull(response); - assertEquals("OK", response.state()); - assertNotNull(response.cert()); - assertEquals("QUALIFIED", response.cert().certificateLevel()); - assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); - } - - @Test - void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { - SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); - - var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", null); - CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); - - assertNotNull(response); - assertEquals("OK", response.state()); - assertNotNull(response.cert()); - assertEquals("QUALIFIED", response.cert().certificateLevel()); - assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); - } - - @Test - void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { - SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(UserAccountNotFoundException.class, - () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); - } - - @Test - void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { - SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); - assertThrows(RelyingPartyAccountConfigurationException.class, - () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); - } - } - - @Nested - @WireMockTest(httpPort = 18089) - class DeviceLinkSignatureTests { - - private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); - - private SmartIdRestConnector connector; - - @BeforeEach - public void setUp() { - WireMock.configureFor("localhost", 18089); - connector = new SmartIdRestConnector("http://localhost:18089"); - } - - @Test - void initDeviceLinkSignature_withSemanticsIdentifier_successful() { - stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); - - assertNotNull(response); - assertEquals("test-session-id", response.sessionID()); - assertEquals("test-session-token", response.sessionToken()); - assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); - } - - @Test - void initDeviceLinkSignature_withDocumentNumber_successful() { - stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - String documentNumber = "PNOEE-31111111111-MOCK-Q"; - - DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); - - assertNotNull(response); - assertEquals("test-session-id", response.sessionID()); - assertEquals("test-session-token", response.sessionToken()); - assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); - assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); - } - - @Test - void initDeviceLinkSignature_userAccountNotFound() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_relyingPartyNoPermission() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_invalidRequest() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - - assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); - } - - @Test - void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initDeviceLinkSignature_throwsSmartIdClientException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); - } - - @Test - void initDeviceLinkSignature_throwsServerMaintenanceException() { - stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); - - DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18084) - class SemanticsIdentifierNotificationSignature { - - private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; - private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18084"); - } - - @Test - void initNotificationSignature_onlyRequiredFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_allFields_ok() { - SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, - "requests/sign/notification/signature/notification-signature-session-request-all-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("DEMO", CertificateLevel.QSCD, "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", true); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { - connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); - }); - } - - @Test - void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - - @Test - void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); - } - - @Test - void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); - } - } - - @Nested - @WireMockTest(httpPort = 18085) - class DocumentNumberNotificationSignature { - - private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; - private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; - - private SmartIdRestConnector connector; - - @BeforeEach - void setUp() { - connector = new SmartIdRestConnector("http://localhost:18085"); - } - - @Test - void initNotificationSignature_onlyRequiredFields() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_allFields() { - SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", - "responses/sign/notification/signature/notification-signature-session-response.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); - - assertSessionResponse(response); - } - - @Test - void initNotificationSignature_badRequest_throwException() { - SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_unauthorized_throwException() { - SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("NOT DEMO", null, null, null); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { - SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_userAccountNotFound_throwException() { - SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, - "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - - @Test - void initNotificationSignature_throwsSmartIdClientException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); - } - - @Test - void initNotificationSignature_throwsServerMaintenanceException() { - SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); - NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); - - assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); - } - } - - private DeviceLinkAuthenticationSessionRequest toQrAuthenticationSessionRequest() { - return toDeviceLinkAuthenticationSessionRequest(null, null); - } - - private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest(RequestProperties requestProperties, - String initialCallbackUrl) { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( - Base64.toBase64String("a".repeat(32).getBytes()), - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - return new DeviceLinkAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - CertificateLevel.QUALIFIED.name(), - SignatureProtocol.ACSP_V2, - signatureProtocolParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), - requestProperties, - null, - initialCallbackUrl - ); - } - - private static NotificationAuthenticationSessionRequest toNotificationAuthenticationSessionRequest(CertificateLevel certificateLevel, RequestProperties requestProperties) { - var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( - Base64.toBase64String("a".repeat(32).getBytes()), - "rsassa-pss", - new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); - - return new NotificationAuthenticationSessionRequest( - "00000000-0000-4000-8000-000000000000", - "DEMO", - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.ACSP_V2.name(), - signatureProtocolParameters, - InteractionUtil.encodeToBase64(List.of(new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Login?"))), - requestProperties, - null, - "numeric4" - ); - } - - private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { - return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); - } - - private static DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { - var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", - "rsassa-pss", - new SignatureAlgorithmParameters("SHA3-512")); - - return new DeviceLinkSignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", - "BANK123", - null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - protocolParameters, - null, - null, - InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign the document", null))), - null, - null); - } - - private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest() { - return toNotificationSignatureSessionRequest("DEMO", null, null, null); - } - - private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest(String relyingPartyName, - CertificateLevel certificateLevel, - String nonce, - Boolean shareIpAddress) { - var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "rsassa-pss", - new SignatureAlgorithmParameters("SHA-512")); - var interaction = new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Sign it!"); - return new NotificationSignatureSessionRequest("00000000-0000-4000-8000-000000000000", - relyingPartyName, - certificateLevel != null ? certificateLevel.name() : null, - SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), - protocolParameters, - nonce, - null, - InteractionUtil.encodeToBase64(List.of(interaction)), - shareIpAddress != null ? new RequestProperties(shareIpAddress) : null); - } - - private static void assertResponseValues(DeviceLinkSessionResponse response, - String expectedSessionToken, - String expectedSessionSecret, - Instant start, - Instant end) { - assertNotNull(response); - assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); - assertEquals(expectedSessionToken, response.sessionToken()); - assertEquals(expectedSessionSecret, response.sessionSecret()); - assertNotNull(response.receivedAt()); - assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); - assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); - } - - private static void assertSessionResponse(NotificationSignatureSessionResponse response) { - assertNotNull(response); - assertNotNull(response.sessionID()); - VerificationCode verificationCode = response.vc(); - assertNotNull(verificationCode); - assertNotNull(verificationCode.type()); - assertNotNull(verificationCode.value()); - } -} +package ee.sk.smartid.rest; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubNotFoundResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostErrorResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubPostRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubRequestWithResponse; +import static ee.sk.smartid.SmartIdRestServiceStubs.stubStrictRequestWithResponse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import ee.sk.smartid.CertificateLevel; +import ee.sk.smartid.HashAlgorithm; +import ee.sk.smartid.SignatureProtocol; +import ee.sk.smartid.SmartIdRestServiceStubs; +import ee.sk.smartid.common.devicelink.interactions.DeviceLinkInteractionType; +import ee.sk.smartid.common.notification.interactions.NotificationInteractionType; +import ee.sk.smartid.exception.SessionNotFoundException; +import ee.sk.smartid.exception.permanent.RelyingPartyAccountConfigurationException; +import ee.sk.smartid.exception.permanent.ServerMaintenanceException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; +import ee.sk.smartid.exception.useraccount.NoSuitableAccountOfRequestedTypeFoundException; +import ee.sk.smartid.exception.useraccount.PersonShouldViewSmartIdPortalException; +import ee.sk.smartid.exception.useraccount.UserAccountNotFoundException; +import ee.sk.smartid.rest.dao.AcspV2SignatureProtocolParameters; +import ee.sk.smartid.rest.dao.CertificateByDocumentNumberRequest; +import ee.sk.smartid.rest.dao.CertificateResponse; +import ee.sk.smartid.rest.dao.DeviceLinkAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.DeviceLinkSessionResponse; +import ee.sk.smartid.rest.dao.DeviceLinkSignatureSessionRequest; +import ee.sk.smartid.rest.dao.Interaction; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionRequest; +import ee.sk.smartid.rest.dao.LinkedSignatureSessionResponse; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionRequest; +import ee.sk.smartid.rest.dao.NotificationAuthenticationSessionResponse; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionRequest; +import ee.sk.smartid.rest.dao.NotificationCertificateChoiceSessionResponse; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionRequest; +import ee.sk.smartid.rest.dao.NotificationSignatureSessionResponse; +import ee.sk.smartid.rest.dao.RawDigestSignatureProtocolParameters; +import ee.sk.smartid.rest.dao.RequestProperties; +import ee.sk.smartid.rest.dao.SemanticsIdentifier; +import ee.sk.smartid.rest.dao.SessionMaskGenAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionSignature; +import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.rest.dao.SignatureAlgorithmParameters; +import ee.sk.smartid.rest.dao.VerificationCode; +import ee.sk.smartid.util.InteractionUtil; + +class SmartIdRestConnectorTest { + + private static final String SESSION_SECRET = "c2Vzc2lvblNlY3JldA=="; + + @Nested + @WireMockTest(httpPort = 18089) + class SessionStatusTests { + + private static final String SERVER_RANDOM = "J0iyCYOu8cTWuoD8rD05IIrZ"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void getSessionStatus_running() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + } + + @Test + void getSessionStatus_running_withIgnoredProperties() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-running-with-ignored-properties.json"); + assertNotNull(sessionStatus); + assertEquals("RUNNING", sessionStatus.getState()); + assertNotNull(sessionStatus.getIgnoredProperties()); + assertEquals(2, sessionStatus.getIgnoredProperties().length); + assertEquals("testingIgnored", sessionStatus.getIgnoredProperties()[0]); + assertEquals("testingIgnoredTwo", sessionStatus.getIgnoredProperties()[1]); + } + + @Test + void getSessionStatus_forSuccessfulAuthenticationRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("displayTextAndPIN", sessionStatus.getInteractionTypeUsed()); + + assertEquals("ACSP_V2", sessionStatus.getSignatureProtocol()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionSignature.getValue())); + assertEquals(SERVER_RANDOM, sessionSignature.getServerRandom()); + assertTrue(Pattern.matches("^[a-zA-Z0-9-_]{43}$", sessionSignature.getUserChallenge())); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + assertSignatureAlgorithmParameters(sessionSignature, "SHA3-512"); + + assertNotNull(sessionStatus.getCert()); + assertTrue(Pattern.matches("^[a-zA-Z0-9+\\/]+={0,2}$", sessionStatus.getCert().getValue())); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_forSuccessfulCertificateRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-certificate-choice.json"); + assertSuccessfulResponse(sessionStatus); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_forSuccessfulSignatureRequest() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-signature.json"); + assertSuccessfulResponse(sessionStatus); + assertEquals("verificationCodeChoice", sessionStatus.getInteractionTypeUsed()); + + assertEquals("RAW_DIGEST_SIGNATURE", sessionStatus.getSignatureProtocol()); + SessionSignature sessionSignature = sessionStatus.getSignature(); + assertNotNull(sessionSignature); + assertThat(sessionSignature.getValue(), startsWith("fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgL")); + assertEquals("QR", sessionSignature.getFlowType()); + assertEquals("rsassa-pss", sessionSignature.getSignatureAlgorithm()); + + assertSignatureAlgorithmParameters(sessionSignature, "SHA-512"); + + assertNotNull(sessionStatus.getCert()); + assertThat(sessionStatus.getCert().getValue(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww")); + assertEquals("QUALIFIED", sessionStatus.getCert().getCertificateLevel()); + } + + @Test + void getSessionStatus_hasUserAgentHeader() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-successful-authentication.json"); + assertSuccessfulResponse(sessionStatus); + + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016")) + .withHeader("User-Agent", containing("smart-id-java-client/")) + .withHeader("User-Agent", containing("Java/"))); + } + + @Test + void getSessionStatus_withTimeoutParameter() { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", "responses/session-status-successful-authentication.json"); + connector.setSessionStatusResponseSocketOpenTime(TimeUnit.SECONDS, 10L); + SessionStatus sessionStatus = connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + assertSuccessfulResponse(sessionStatus); + verify(getRequestedFor(urlEqualTo("/session/de305d54-75b4-431b-adb2-eb6b9e546016?timeoutMs=10000"))); + } + + @Test + void getSessionStatus_whenSessionNotFound() { + assertThrows(SessionNotFoundException.class, () -> { + stubNotFoundResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016"); + connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + }); + } + + @Nested + class UserRefusedInteractions { + + @Test + void getSessionStatus_userHasRefused() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessage() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessage", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedConfirmationMessageWithVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-confirmation-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("confirmationMessageAndVerificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedDisplayTextAndPin() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-display-text-and-pin.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("displayTextAndPIN", sessionStatus.getResult().getDetails().getInteraction()); + } + + @Test + void getSessionStatus_userHasRefusedVerificationCodeChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-vc-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_INTERACTION", sessionStatus.getResult().getEndResult()); + assertEquals("verificationCodeChoice", sessionStatus.getResult().getDetails().getInteraction()); + } + } + + @Test + void getSessionStatus_userHasRefusedCertChoice() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-user-refused-cert-choice.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("USER_REFUSED_CERT_CHOICE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_timeout() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-timeout.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("TIMEOUT", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_userHasSelectedWrongVcCode() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-wrong-vc.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("WRONG_VC", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_documentUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-document-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("DOCUMENT_UNUSABLE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_protocolFailure() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-protocol-failure.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("PROTOCOL_FAILURE", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_expectedLinkedSession() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-expected-linked-session.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("EXPECTED_LINKED_SESSION", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_serverError() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-server-error.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("SERVER_ERROR", sessionStatus.getResult().getEndResult()); + } + + @Test + void getSessionStatus_accountUnusable() { + SessionStatus sessionStatus = getStubbedSessionStatusWithResponse("responses/session-status-account-unusable.json"); + assertEquals("COMPLETE", sessionStatus.getState()); + assertEquals("ACCOUNT_UNUSABLE", sessionStatus.getResult().getEndResult()); + } + + private SessionStatus getStubbedSessionStatusWithResponse(String responseFile) { + stubRequestWithResponse("/session/de305d54-75b4-431b-adb2-eb6b9e546016", responseFile); + return connector.getSessionStatus("de305d54-75b4-431b-adb2-eb6b9e546016"); + } + + private static void assertSuccessfulResponse(SessionStatus sessionStatus) { + assertEquals("COMPLETE", sessionStatus.getState()); + assertNotNull(sessionStatus.getResult()); + assertEquals("OK", sessionStatus.getResult().getEndResult()); + assertEquals("PNOEE-40504040001-MOCK-Q", sessionStatus.getResult().getDocumentNumber()); + } + + private static void assertSignatureAlgorithmParameters(SessionSignature sessionSignature, String expectedHashAlgorithm) { + SessionSignatureAlgorithmParameters signatureAlgorithmParameters = sessionSignature.getSignatureAlgorithmParameters(); + assertEquals(expectedHashAlgorithm, signatureAlgorithmParameters.getHashAlgorithm()); + var maskGenAlgorithm = signatureAlgorithmParameters.getMaskGenAlgorithm(); + assertEquals("id-mgf1", maskGenAlgorithm.getAlgorithm()); + SessionMaskGenAlgorithmParameters parameters = maskGenAlgorithm.getParameters(); + assertEquals(expectedHashAlgorithm, parameters.getHashAlgorithm()); + assertEquals(64, signatureAlgorithmParameters.getSaltLength()); + assertEquals("0xbc", signatureAlgorithmParameters.getTrailerField()); + } + } + + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierDeviceLinkAuthentication { + + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/device-link/etsi/PNOEE-30303039914"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-30303039914"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toQrAuthenticationSessionRequest(); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_sameDeviceOnlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(null, "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_sameDeviceAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + var deviceLinkAuthenticationSessionRequest = toDeviceLinkAuthenticationSessionRequest(new RequestProperties(true), "https://example.com/callback"); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(deviceLinkAuthenticationSessionRequest, SEMANTICS_IDENTIFIER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, () -> + connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toQrAuthenticationSessionRequest(), SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberDeviceLinkAuthentication { + + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/device-link/document/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initDeviceLinkAuthentication() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), "PNOEE-48010010101-MOCK-Q")); + } + + @Test + void initDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + } + + @Nested + @WireMockTest(httpPort = 18081) + class AnonymousDeviceLinkAuthentication { + + private static final String ANONYMOUS_AUTHENTICATION_PATH = "/authentication/device-link/anonymous"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18081"); + } + + @Test + void initAnonymousDeviceLinkAuthentication_qrCodeFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initAnonymousDeviceLinkAuthentication_sameDeviceFlow_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(ANONYMOUS_AUTHENTICATION_PATH, + "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", + "responses/auth/device-link/device-link-authentication-session-response.json"); + + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null)); + Instant end = Instant.now(); + + assertResponseValues(response, "sessionToken", SESSION_SECRET, start, end); + } + + @Test + void initAnonymousDeviceLinkAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-invalid-request.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_requestIsUnauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + + @Test + void initAnonymousDeviceLinkAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(ANONYMOUS_AUTHENTICATION_PATH, "requests/auth/device-link/device-link-authentication-session-request-qr-code.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initAnonymousDeviceLinkAuthentication(toDeviceLinkAuthenticationSessionRequest(null, null))); + } + } + + @Nested + @WireMockTest(httpPort = 18082) + class SemanticsIdentifierNotificationAuthentication { + + private static final String AUTHENTICATION_WITH_PERSON_CODE_PATH = "/authentication/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18082"); + } + + @Test + void initNotificationAuthentication_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), SEMANTICS_IDENTIFIER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + assertThrows(SmartIdClientException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_PERSON_CODE_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18083) + class DocumentNumberNotificationAuthentication { + + private static final String AUTHENTICATION_WITH_DOCUMENT_NR_PATH = "/authentication/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18083"); + } + + @Test + void initNotificationAuthentication_onlyRequeriedFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_allFields_ok() { + SmartIdRestServiceStubs.stubRequestWithResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, + "requests/auth/notification/notification-authentication-session-request-all-fields.json", + "responses/auth/notification/notification-session-response.json"); + + NotificationAuthenticationSessionResponse response = connector.initNotificationAuthentication( + toNotificationAuthenticationSessionRequest(CertificateLevel.QUALIFIED, new RequestProperties(true)), DOCUMENT_NUMBER); + + assertNotNull(response); + } + + @Test + void initNotificationAuthentication_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-invalid.json"); + + var authenticationRequest = new NotificationAuthenticationSessionRequest("00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(authenticationRequest, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> + connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_forbiddenForRP_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + + @Test + void initNotificationAuthentication_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(AUTHENTICATION_WITH_DOCUMENT_NR_PATH, "requests/auth/notification/notification-authentication-session-request-only-required-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initNotificationAuthentication(toNotificationAuthenticationSessionRequest(null, null), DOCUMENT_NUMBER)); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkCertificateChoiceTests { + + private static final String ANONYMOUS_CERTIFICATE_CHOICE_PATH = "/signature/certificate-choice/device-link/anonymous"; + + private SmartIdConnector connector; + + @BeforeEach + public void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initDeviceLinkCertificateChoice() { + stubPostRequestWithResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, "responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json"); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + Instant start = Instant.now(); + DeviceLinkSessionResponse response = connector.initDeviceLinkCertificateChoice(request); + Instant end = Instant.now(); + + assertResponseValues(response, "sampleSessionToken", "sampleSessionSecret", start, end); + } + + @Test + void initDeviceLinkCertificateChoice_invalidCertificateLevel_throwsBadRequestException() { + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_userAccountNotFound() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 404); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_relyingPartyNoPermission() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 403); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_invalidRequest() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 400); + + var request = new DeviceLinkCertificateChoiceSessionRequest("", "", null, null, null, null, null); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 401); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + var exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/certificate-choice/device-link/anonymous", exception.getMessage()); + } + + @Test + void initDeviceLinkCertificateChoice_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 471); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 472); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + @Test + void initDeviceLinkCertificateChoice_throwsSmartIdClientException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 480); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); + } + + @Test + void initDeviceLinkCertificateChoice_throwsServerMaintenanceException() { + stubPostErrorResponse(ANONYMOUS_CERTIFICATE_CHOICE_PATH, 580); + + DeviceLinkCertificateChoiceSessionRequest request = toCertificateChoiceSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkCertificateChoice(request)); + } + + private static DeviceLinkCertificateChoiceSessionRequest toCertificateChoiceSessionRequest() { + return new DeviceLinkCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + "ADVANCED", + null, + null, + null, + null + ); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class LinkedNotificationSignature { + + private static final String LINKED_SIGNATURE_PATH = "/signature/notification/linked/PNOEE-31111111111-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-31111111111-MOCK-Q"; + private static final String NONCE = "cmFuZG9tTm9uY2U="; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initLinkedNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionRequest request = toLinkedSignatureSessionRequest(null, null, null); + LinkedSignatureSessionResponse linkedSignatureSessionResponse = connector.initLinkedNotificationSignature(request, DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_withAllFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", "responses/sign/linked/signature/linked-notification-signature-session-response.json"); + + LinkedSignatureSessionResponse linkedSignatureSessionResponse = + connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER); + + assertNotNull(linkedSignatureSessionResponse); + assertEquals("00000000-0000-0000-0000-000000000000", linkedSignatureSessionResponse.sessionID()); + } + + @Test + void initLinkedNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_rpNotAllowedToMakeTheRequest_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_accountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json"); + + assertThrows(UserAccountNotFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 471); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_issueWithUserAccount_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 472); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> { + var linkedSignatureSessionRequest = toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, "cmFuZG9tTm9uY2U=", new RequestProperties(true)); + connector.initLinkedNotificationSignature(linkedSignatureSessionRequest, DOCUMENT_NUMBER); + }); + } + + @Test + void initLinkedNotificationSignature_apiClientBeingUsedIsOutdated_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 480); + + assertThrows(SmartIdClientException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + @Test + void initLinkedNotificationSignature_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubErrorResponse(LINKED_SIGNATURE_PATH, "requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json", 580); + + assertThrows(ServerMaintenanceException.class, + () -> connector.initLinkedNotificationSignature(toFullLinkedSignatureSessionRequest(), DOCUMENT_NUMBER)); + } + + private LinkedSignatureSessionRequest toFullLinkedSignatureSessionRequest() { + return toLinkedSignatureSessionRequest(CertificateLevel.QUALIFIED, NONCE, new RequestProperties(true)); + } + + private static LinkedSignatureSessionRequest toLinkedSignatureSessionRequest(CertificateLevel certificateLevel, + String nonce, + RequestProperties requestProperties) { + var rawDigestSignatureProtocolParameters = new RawDigestSignatureProtocolParameters( + "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA_512.getAlgorithmName())); + return new LinkedSignatureSessionRequest("00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel != null ? certificateLevel.name() : null, + "RAW_DIGEST_SIGNATURE", + rawDigestSignatureProtocolParameters, + "10000000-0000-000-000-000000000000", + nonce, + "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + requestProperties, + null); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class SemanticsIdentifierNotificationCertificateChoiceTests { + + private static final String CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH = "/signature/certificate-choice/notification/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-31111111111"); + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initCertificateChoice_onlyRequiredFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @Test + void initCertificateChoice_allFields_successful() { + stubStrictRequestWithResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json", + "responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json"); + var request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + "QUALIFIED", + "cmFuZG9tTm9uY2U=", + null, + new RequestProperties(true)); + SemanticsIdentifier semanticsIdentifier = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + NotificationCertificateChoiceSessionResponse response = connector.initNotificationCertificateChoice(request, semanticsIdentifier); + + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + } + + @Test + void initCertificateChoice_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + null, + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, "requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_rpDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + null, + null, + null, + null); + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_suitableAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 471); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_userShouldCheckPortal_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 472); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_javaClientBeingUsedIsTooOld_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 480); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(SmartIdClientException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initCertificateChoice_systemUnderMaintenance_throwException() { + SmartIdRestServiceStubs.stubPostErrorResponse(CERTIFICATE_CHOICE_WITH_PERSON_CODE_PATH, 580); + + NotificationCertificateChoiceSessionRequest request = new NotificationCertificateChoiceSessionRequest( + "00000000-0000-4000-8000-000000000000", + "NOT DEMO", + null, + null, + null, + null); + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationCertificateChoice(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18086) + class CertificateByDocumentNumberTests { + + private static final String CERTIFICATE_BY_DOCUMENT_NUMBER_PATH = "/signature/certificate/PNOEE-30303039914-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-30303039914-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18086"); + } + + @Test + void getCertificateByDocumentNumber_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json", "responses/certificate-by-document-number-response.json"); + + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest()); + + assertNotNull(response); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + } + + @Test + void getCertificateByDocumentNumber_certificateLevelNotSet_successful() { + SmartIdRestServiceStubs.stubRequestWithResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-only-required-fields.json", "responses/certificate-by-document-number-response.json"); + + var certificateByDocumentNumberRequest = new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", null); + CertificateResponse response = connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, certificateByDocumentNumberRequest); + + assertNotNull(response); + assertEquals("OK", response.state()); + assertNotNull(response.cert()); + assertEquals("QUALIFIED", response.cert().certificateLevel()); + assertThat(response.cert().value(), startsWith("MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30")); + } + + @Test + void getCertificateByDocumentNumber_userAccountNotFound_throwsException() { + SmartIdRestServiceStubs.stubNotFoundResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); + assertThrows(UserAccountNotFoundException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); + } + + @Test + void getCertificateByDocumentNumber_requestUnauthorized_throwsException() { + SmartIdRestServiceStubs.stubForbiddenResponse(CERTIFICATE_BY_DOCUMENT_NUMBER_PATH, "requests/sign/certificate-by-document-number-request-all-fields.json"); + assertThrows(RelyingPartyAccountConfigurationException.class, + () -> connector.getCertificateByDocumentNumber(DOCUMENT_NUMBER, toCertificateByDocumentNumberRequest())); + } + } + + @Nested + @WireMockTest(httpPort = 18089) + class DeviceLinkSignatureTests { + + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/device-link/etsi/PNOEE-31111111111"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNO", "EE", "31111111111"); + + private SmartIdRestConnector connector; + + @BeforeEach + public void setUp() { + WireMock.configureFor("localhost", 18089); + connector = new SmartIdRestConnector("http://localhost:18089"); + } + + @Test + void initDeviceLinkSignature_withSemanticsIdentifier_successful() { + stubPostRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "responses/sign/device-link/signature/device-link-signature-session-response.json"); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER); + + assertNotNull(response); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); + } + + @Test + void initDeviceLinkSignature_withDocumentNumber_successful() { + stubPostRequestWithResponse("/signature/device-link/document/PNOEE-31111111111-MOCK-Q", "responses/sign/device-link/signature/device-link-signature-session-response.json"); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + String documentNumber = "PNOEE-31111111111-MOCK-Q"; + + DeviceLinkSessionResponse response = connector.initDeviceLinkSignature(request, documentNumber); + + assertNotNull(response); + assertEquals("test-session-id", response.sessionID()); + assertEquals("test-session-token", response.sessionToken()); + assertEquals("c2Vzc2lvblNlY3JldA==", response.sessionSecret()); + assertEquals(URI.create("https://smart-id.com/device-link/"), response.deviceLinkBase()); + } + + @Test + void initDeviceLinkSignature_userAccountNotFound() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 404); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_relyingPartyNoPermission() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 403); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_invalidRequest() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 400); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsRelyingPartyAccountConfigurationException_whenUnauthorized() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 401); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + Exception exception = assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + + assertEquals("Request is unauthorized for URI http://localhost:18089/signature/device-link/etsi/PNOEE-31111111111", exception.getMessage()); + } + + @Test + void initDeviceLinkSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsPersonShouldViewSmartIdPortalException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initDeviceLinkSignature_throwsSmartIdClientException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + var exception = assertThrows(SmartIdClientException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + assertEquals("Client-side API is too old and not supported anymore", exception.getMessage()); + } + + @Test + void initDeviceLinkSignature_throwsServerMaintenanceException() { + stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); + + DeviceLinkSignatureSessionRequest request = createSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initDeviceLinkSignature(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18084) + class SemanticsIdentifierNotificationSignature { + + private static final String SIGNATURE_WITH_PERSON_CODE_PATH = "/signature/notification/etsi/PNOEE-48010010101"; + private static final SemanticsIdentifier SEMANTICS_IDENTIFIER = new SemanticsIdentifier("PNOEE-48010010101"); + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18084"); + } + + @Test + void initNotificationSignature_onlyRequiredFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_allFields_ok() { + SmartIdRestServiceStubs.stubStrictRequestWithResponse(SIGNATURE_WITH_PERSON_CODE_PATH, + "requests/sign/notification/signature/notification-signature-session-request-all-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("DEMO", CertificateLevel.QSCD, "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", true); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_PERSON_CODE_PATH, "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 471); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> { + connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER); + }); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 472); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 480); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_PERSON_CODE_PATH, 580); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, SEMANTICS_IDENTIFIER)); + } + } + + @Nested + @WireMockTest(httpPort = 18085) + class DocumentNumberNotificationSignature { + + private static final String SIGNATURE_WITH_DOCUMENT_NUMBER_PATH = "/signature/notification/document/PNOEE-48010010101-MOCK-Q"; + private static final String DOCUMENT_NUMBER = "PNOEE-48010010101-MOCK-Q"; + + private SmartIdRestConnector connector; + + @BeforeEach + void setUp() { + connector = new SmartIdRestConnector("http://localhost:18085"); + } + + @Test + void initNotificationSignature_onlyRequiredFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_allFields() { + SmartIdRestServiceStubs.stubRequestWithResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json", + "responses/sign/notification/signature/notification-signature-session-response.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + NotificationSignatureSessionResponse response = connector.initNotificationSignature(request, DOCUMENT_NUMBER); + + assertSessionResponse(response); + } + + @Test + void initNotificationSignature_badRequest_throwException() { + SmartIdRestServiceStubs.stubBadRequestResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_unauthorized_throwException() { + SmartIdRestServiceStubs.stubUnauthorizedResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest("NOT DEMO", null, null, null); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_relyingPartyDoesNotHavePermission_throwException() { + SmartIdRestServiceStubs.stubForbiddenResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(RelyingPartyAccountConfigurationException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_userAccountNotFound_throwException() { + SmartIdRestServiceStubs.stubNotFoundResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, + "requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json"); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(UserAccountNotFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsNoSuitableAccountOfRequestedTypeFoundException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 471); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(NoSuitableAccountOfRequestedTypeFoundException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsPersonShouldViewSmartIdPortalException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 472); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(PersonShouldViewSmartIdPortalException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + + @Test + void initNotificationSignature_throwsSmartIdClientException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 480); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + var ex = assertThrows(SmartIdClientException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + assertEquals("Client-side API is too old and not supported anymore", ex.getMessage()); + } + + @Test + void initNotificationSignature_throwsServerMaintenanceException() { + SmartIdRestServiceStubs.stubPostErrorResponse(SIGNATURE_WITH_DOCUMENT_NUMBER_PATH, 580); + NotificationSignatureSessionRequest request = toNotificationSignatureSessionRequest(); + + assertThrows(ServerMaintenanceException.class, () -> connector.initNotificationSignature(request, DOCUMENT_NUMBER)); + } + } + + private DeviceLinkAuthenticationSessionRequest toQrAuthenticationSessionRequest() { + return toDeviceLinkAuthenticationSessionRequest(null, null); + } + + private static DeviceLinkAuthenticationSessionRequest toDeviceLinkAuthenticationSessionRequest(RequestProperties requestProperties, + String initialCallbackUrl) { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + return new DeviceLinkAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + CertificateLevel.QUALIFIED.name(), + SignatureProtocol.ACSP_V2, + signatureProtocolParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Log in?", null))), + requestProperties, + null, + initialCallbackUrl + ); + } + + private static NotificationAuthenticationSessionRequest toNotificationAuthenticationSessionRequest(CertificateLevel certificateLevel, RequestProperties requestProperties) { + var signatureProtocolParameters = new AcspV2SignatureProtocolParameters( + Base64.toBase64String("a".repeat(32).getBytes()), + "rsassa-pss", + new SignatureAlgorithmParameters(HashAlgorithm.SHA3_512.getAlgorithmName())); + + return new NotificationAuthenticationSessionRequest( + "00000000-0000-4000-8000-000000000000", + "DEMO", + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.ACSP_V2.name(), + signatureProtocolParameters, + InteractionUtil.encodeToBase64(List.of(new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Login?"))), + requestProperties, + null, + "numeric4" + ); + } + + private static CertificateByDocumentNumberRequest toCertificateByDocumentNumberRequest() { + return new CertificateByDocumentNumberRequest("00000000-0000-4000-8000-000000000000", "DEMO", "ADVANCED"); + } + + private static DeviceLinkSignatureSessionRequest createSignatureSessionRequest() { + var protocolParameters = new RawDigestSignatureProtocolParameters("base64-encoded-digest", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA3-512")); + + return new DeviceLinkSignatureSessionRequest("de305d54-75b4-431b-adb2-eb6b9e546014", + "BANK123", + null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + null, + null, + InteractionUtil.encodeToBase64(List.of(new Interaction(DeviceLinkInteractionType.DISPLAY_TEXT_AND_PIN.getCode(), "Sign the document", null))), + null, + null); + } + + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest() { + return toNotificationSignatureSessionRequest("DEMO", null, null, null); + } + + private static NotificationSignatureSessionRequest toNotificationSignatureSessionRequest(String relyingPartyName, + CertificateLevel certificateLevel, + String nonce, + Boolean shareIpAddress) { + var protocolParameters = new RawDigestSignatureProtocolParameters("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "rsassa-pss", + new SignatureAlgorithmParameters("SHA-512")); + var interaction = new Interaction(NotificationInteractionType.CONFIRMATION_MESSAGE.getCode(), null, "Sign it!"); + return new NotificationSignatureSessionRequest("00000000-0000-4000-8000-000000000000", + relyingPartyName, + certificateLevel != null ? certificateLevel.name() : null, + SignatureProtocol.RAW_DIGEST_SIGNATURE.name(), + protocolParameters, + nonce, + null, + InteractionUtil.encodeToBase64(List.of(interaction)), + shareIpAddress != null ? new RequestProperties(shareIpAddress) : null); + } + + private static void assertResponseValues(DeviceLinkSessionResponse response, + String expectedSessionToken, + String expectedSessionSecret, + Instant start, + Instant end) { + assertNotNull(response); + assertEquals("00000000-0000-0000-0000-000000000000", response.sessionID()); + assertEquals(expectedSessionToken, response.sessionToken()); + assertEquals(expectedSessionSecret, response.sessionSecret()); + assertNotNull(response.receivedAt()); + assertFalse(response.receivedAt().isBefore(start.minusSeconds(1))); + assertFalse(response.receivedAt().isAfter(end.plusSeconds(1))); + } + + private static void assertSessionResponse(NotificationSignatureSessionResponse response) { + assertNotNull(response); + assertNotNull(response.sessionID()); + VerificationCode verificationCode = response.vc(); + assertNotNull(verificationCode); + assertNotNull(verificationCode.type()); + assertNotNull(verificationCode.value()); + } +} diff --git a/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java index fbf3810a..fd1b36f9 100644 --- a/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CallbackUrlUtilTest.java @@ -1,105 +1,105 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -import ee.sk.smartid.common.devicelink.CallbackUrl; -import ee.sk.smartid.exception.SessionSecretMismatchException; -import ee.sk.smartid.exception.permanent.SmartIdClientException; - -class CallbackUrlUtilTest { - - private static final String SESSION_SECRET_DIGEST = "nKMc7gT3mvWuJtfXVFjCY2ehuvTs26f1Sgjk6g9oOr8"; - - @Nested - class CreateCallbackUrl { - - @Test - void createCallbackUrl_valueQueryParameterIsSameAsUrlToken() { - CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); - - assertEquals("https://example.com/callback?value=" + callbackUrl.urlToken(), - callbackUrl.initialCallbackUri().toString()); - } - - @ParameterizedTest - @NullAndEmptySource - void createCallbackUrl_inputBaseUrlIsEmpty_throwException(String baseUrl) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.createCallbackUrl(baseUrl)); - assertEquals("Parameter for 'baseUrl' cannot be empty", ex.getMessage()); - } - } - - @Nested - class ValidateSessionSecretDigest { - - @Test - void validateSessionSecretDigest() { - String sessionSecret = "fBo1/L1vM9xcSmZF7hvvooEj"; - assertDoesNotThrow(() -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - } - - @ParameterizedTest - @NullAndEmptySource - void validateSessionSecretDigest_sessionSecretDigestIsEmpty_throwException(String sessionSecretDigest) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(sessionSecretDigest, "")); - assertEquals("Parameter for 'sessionSecretDigest' cannot be empty", ex.getMessage()); - } - - @ParameterizedTest - @NullAndEmptySource - void validateSessionSecretDigest_sessionSecretIsEmpty_throwException(String sessionSecret) { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - assertEquals("Parameter for 'sessionSecret' cannot be empty", ex.getMessage()); - } - - @Test - void validateSessionSecretDigest_sessionSecretValidationFails_throwException() { - String sessionSecret = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); - - var ex = assertThrows(SessionSecretMismatchException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); - assertEquals("Session secret digest from callback does not match calculated session secret digest", ex.getMessage()); - } - - @Test - void validateSessionSecretDigest_sessionSecretIsNotBase64Encoded_throwException() { - var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, "sessionSecret")); - assertEquals("Parameter 'sessionSecret' is not Base64-encoded value", ex.getMessage()); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import ee.sk.smartid.common.devicelink.CallbackUrl; +import ee.sk.smartid.exception.SessionSecretMismatchException; +import ee.sk.smartid.exception.permanent.SmartIdClientException; + +class CallbackUrlUtilTest { + + private static final String SESSION_SECRET_DIGEST = "nKMc7gT3mvWuJtfXVFjCY2ehuvTs26f1Sgjk6g9oOr8"; + + @Nested + class CreateCallbackUrl { + + @Test + void createCallbackUrl_valueQueryParameterIsSameAsUrlToken() { + CallbackUrl callbackUrl = CallbackUrlUtil.createCallbackUrl("https://example.com/callback"); + + assertEquals("https://example.com/callback?value=" + callbackUrl.urlToken(), + callbackUrl.initialCallbackUri().toString()); + } + + @ParameterizedTest + @NullAndEmptySource + void createCallbackUrl_inputBaseUrlIsEmpty_throwException(String baseUrl) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.createCallbackUrl(baseUrl)); + assertEquals("Parameter for 'baseUrl' cannot be empty", ex.getMessage()); + } + } + + @Nested + class ValidateSessionSecretDigest { + + @Test + void validateSessionSecretDigest() { + String sessionSecret = "fBo1/L1vM9xcSmZF7hvvooEj"; + assertDoesNotThrow(() -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretDigestIsEmpty_throwException(String sessionSecretDigest) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(sessionSecretDigest, "")); + assertEquals("Parameter for 'sessionSecretDigest' cannot be empty", ex.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void validateSessionSecretDigest_sessionSecretIsEmpty_throwException(String sessionSecret) { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Parameter for 'sessionSecret' cannot be empty", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretValidationFails_throwException() { + String sessionSecret = Base64.getEncoder().encodeToString("sessionSecret".getBytes(StandardCharsets.UTF_8)); + + var ex = assertThrows(SessionSecretMismatchException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, sessionSecret)); + assertEquals("Session secret digest from callback does not match calculated session secret digest", ex.getMessage()); + } + + @Test + void validateSessionSecretDigest_sessionSecretIsNotBase64Encoded_throwException() { + var ex = assertThrows(SmartIdClientException.class, () -> CallbackUrlUtil.validateSessionSecretDigest(SESSION_SECRET_DIGEST, "sessionSecret")); + assertEquals("Parameter 'sessionSecret' is not Base64-encoded value", ex.getMessage()); + } + } +} diff --git a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java index aca5b143..3e742f4e 100644 --- a/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/CertificateAttributeUtilTest.java @@ -1,142 +1,142 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; - -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.InvalidCertificateGenerator; - -public class CertificateAttributeUtilTest { - - private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; - private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; - - @Test - public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { - X509Certificate certificateWithDob = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - - LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); - - assertThat(dateOfBirthCertificateAttribute, is(notNullValue())); - assertThat(dateOfBirthCertificateAttribute, is(LocalDate.of(1903, 3, 3))); - } - - @Test - public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { - X509Certificate certificateWithoutDobAttribute = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); - - LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); - - assertThat(dateOfBirthCertificateAttribute, is(nullValue())); - } - - @ParameterizedTest - @ArgumentsSource(AttributeArgumentProvider.class) - void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - - Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); - - assertTrue(attributeValue.isPresent()); - assertThat(attributeValue.get(), is(expectedValue)); - } - - @Test - void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); - String distinguishedName = certificate.getSubjectX500Principal().getName(); - - Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); - - assertTrue(attributeValue.isEmpty()); - } - - @Test - void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { - X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); - - Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); - - assertTrue(certificatePolicy.isEmpty()); - } - - @Test - void getCertificatePolicy_certificatePolicyPresent() throws CertificateException { - X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); - - Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); - - assertThat(certificatePolicy, contains("1.3.6.1.4.1.10015.3.17.2", "0.4.0.2042.1.1")); - } - - @Test - void hasNonRepudiation_KeyUsageExtensionIsMissing() { - X509Certificate certificate = InvalidCertificateGenerator.builder() - .withKeyUsage(null) - .createCertificate(); - - assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); - } - - private static class AttributeArgumentProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of(Named.of("Given name", BCStyle.GIVENNAME), "BOD"), - Arguments.of(Named.of("Surname", BCStyle.SURNAME), "TESTNUMBER"), - Arguments.of(Named.of("Serial number", BCStyle.SERIALNUMBER), "PNOLV-329999-99901"), - Arguments.of(Named.of("Country", BCStyle.C), "LV") - ); - } - } -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.InvalidCertificateGenerator; + +public class CertificateAttributeUtilTest { + + private static final String AUTH_CERTIFICATE_LV_WITH_DOB = "MIIIpDCCBoygAwIBAgIQSADgqesOeFFhSzm98/SC0zANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwOTIyMTQxMjEzWhcNMjQwOTIyMTQxMjEzWjBmMQswCQYDVQQGEwJMVjEXMBUGA1UEAwwOVEVTVE5VTUJFUixCT0QxEzARBgNVBAQMClRFU1ROVU1CRVIxDDAKBgNVBCoMA0JPRDEbMBkGA1UEBRMSUE5PTFYtMzI5OTk5LTk5OTAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEApkGnh6imYQXES9PP2BGBwwX07KtViUOFffiQgW2WJ8k8UYFgVcjhSRWxz/JaYCtjnDYMa+BKrFShGIUFT78rtFy8HhHFYkQUmybLovv+YiJE3Opm5ppwbfgBq00mxsSTj173uTQYuAbiv0aMVUOjFuKRbUgRXccNhabX+l/3ZNnd0R2Jtyv686HUmtr4pe1ZR8rLM1MAurk35SKK9U6VH3cD3AeKhOQT0cQNFEkFhOhfJ2mANTHH4WkUlqVp4OmIv3NYrtzKZNSgdoj5wcM8/PXuzhvyQu2ejv2Pejlv7ZNftrqoWWBvz3WxJds1fWWBdRkipYHHPkUORRY72UoR0QOixnYizjD5wacQmG96FGWjb+EFJMHjkTde4lAfMfbZJA9cAXpsTl/KZIHNt/nDd/KtpJY/8STgGbyp6Su/vfMlX/oCZHX9hb+t3HD/XQAeDmngZSxKdJ5K8gffB8ZxYYcdk3n7HdULnV22Q56jwUZUSONewIqgwf892XwR3CMySaciMn0Wjf8T40CwzABf1Ih/TAt1v3Xr9uvM1c6fqdvBPPbLXhKzK+paGWxhgZjIaYJ3+AtRW3mYZNY/j4ZAlQMaX2MY5/AEaHoF/fA7+OZ0BX9JGuf1Reos/3pS3v7yiU2+50yF6PgzU5C/wHQJ+9Qh5rAafrAwMdhxUtWU9LS+INBzhbFD9U9waYNsG5lp/WhRGGa4hrtgqeGwHcJflO1+HQCmWzMS/peAJZCnCEHLUkRq4rjvzTETgK1cDXqHoiseW5twcbY9qqmmGvP1MzfBHUJfwYq4EdO8ITRVHLhrqGUmDyGiawZXLv2VQW7s/dRxAmesTFCZ2fNrsC3gdrr7ugVJEFYG9LsN9BvWkC3EE380+UnKc9ZLdnp0qGV+yr9xAUchb7EQTjPaVo/O144IfK8eAFNcTLJP7nbYkn8csRDuBqtKo1m+ZC9HcOKXJ2Zs2lfH+FjxEDaLhre3VyYZorQa5arNd9KdZ47QsJUrspz5P8L3vN70e4dR/lZXAgMBAAGjggJKMIICRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBdBgNVHSAEVjBUMEcGCisGAQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvQ1BTLzAJBgcEAIvsQAECMB0GA1UdDgQWBBTo4aTlpOaClkVVIEL8qAP3iwEvczCBrgYIKwYBBQUHAQMEgaEwgZ4wCAYGBACORgEBMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwEwYGBACORgEGMAkGBwQAjkYBBgEwXAYGBACORgEFMFIwUBZKaHR0cHM6Ly9za2lkc29sdXRpb25zLmV1L2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMAgGBgQAjkYBBDAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDAxBgNVHREEKjAopCYwJDEiMCAGA1UEAwwZUE5PTFYtMzI5OTk5LTk5OTAxLUFBQUEtUTAoBgNVHQkEITAfMB0GCCsGAQUFBwkBMREYDzE5MDMwMzAzMTIwMDAwWjANBgkqhkiG9w0BAQsFAAOCAgEAmOJs32k4syJorWQ0p9EF/yTr3RXO2/U8eEBf6pAw8LPOERy7MX1WtLaTHSctvrzpu37Tcz3B0XhTg7bCcVpn2iZVkDK+2SVLHG8CXLBNXzE5a9C2oUwUtZ9zwIK8gnRtj9vuSoI9oMvNfI0De/e1Y7oZesmUsef3Yavqp2x+qu9Gbup7U5owxpT413Ed65RQvfEGb5FStk7lF6tsT/L8fdhVDXCyat/yY6OQly8OvlxZnrOUGDgdjIxz4u+ZH1InhX9x17TEugXzgZO/3huZkxPkuXwp7CWOtP0/fliSrInS5zbcAfCSB5HZUtR4t4wApWTJ4+AQK/P10skynzJA0k0NbRTFfz8GEZ6ZhgEjwPjThXhoAuSHBPNqToYfy3ar5e7ucPh4SHd0KcUt3rty8/nFgVQd+/Ho6IciVYNAP6TAXuR9tU5XnX8dQWIzjg+wPwSpRr7WvW88qqncpVT4cdjmL+XJRjoK/czsQwfp9FRc23tOWG33dxiIj4lwmlWjPGeBVgp5tgrzAF1P4q+S6IHs70LOOztTF64fHN2YH/gjvb/T7G4oj98b7VTuGmiN7XQhULIdnqG6Kt8GKkkdjp1NziCa04vDOljr2PlChVulNujdNgVDxVfXU5RXP/HgoX2QJtQJyHZwLKvQQfw7T40C6mcN99lsLTx7/xss4Xc="; + private static final String AUTH_CERTIFICATE_LV = "MIIHODCCBSCgAwIBAgIQPLHB9H+omMlZpm/Sy5VpXTANBgkqhkiG9w0BAQsFADArMSkwJwYDVQQDDCBOb3J0YWwgRUlEMTYgQ2VydGlmaWNhdGUgU2lnbmluZzAeFw0xNzA4MzAwNzU3MDZaFw0yMDA4MzAwNzU3MDZaMIGxMQswCQYDVQQGEwJMVjFGMEQGA1UEAww9U1VSTkFNRS0wMTAxMTctMjEyMzQsRk9SRU5BTUUtMDEwMTE3LTIxMjM0LFBOT0xWLTAxMDExNy0yMTIzNDEdMBsGA1UEBAwUU1VSTkFNRS0wMTAxMTctMjEyMzQxHjAcBgNVBCoMFUZPUkVOQU1FLTAxMDExNy0yMTIzNDEbMBkGA1UEBRMSUE5PTFYtMDEwMTE3LTIxMjM0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4vkJlVydzlAmaWCr1d0F8/uSFqGlQ+xkFAO60i60R5XNmT3iltfO2Z/R8g0jDxN1EuJihLc9I3ZQCMLyLF40vnWQkOGxrWEvJy1rTiuGvYXOWBK5JpokJl5KrB6MCRiZbuV9nPCCQ4wnKwC6B9+lLeIPaUm9xsOqEOgqXBVSn7VY9kUx0Peq2ZjCiIYerbMZUGsrCspiZqIYZSU97efxHRQuS46jO3R+HAu4NG6pbQf4PT7QuMCaL8EthvR6d27rZSe8xmg2vvoj7loWUvYqGV+rKgXHmD8tmshYDeYHtdmDkRqbLLsAFEtQ52A8fvHUDFyt+KrHB/g4RQcxeA79Yc6qxuN7zAzKSwfGjt9vdO2ex1LlMAEC99O7O5sMwoPoDXGc6dnlNGY8Ligonyp0KXIAeJ/qIbutjmheK+qk7q2wSPyrLg52aoU3o8l8Us95ftTrouCDsHIKgeG7x6s6H9jTRGYkfxsbEJKLJt+TlBGfLPF7cjgH/H2Mfjshx8GuHnJsrFDHPhrmL0SRKoD7E3Z2IyOS4c5btZiU2SZIkuIuKixOHl4zml8OI3au/VvYXRNDmUi4BWg0WMX8pIGkpOXgk/TY7+/zbOklpAddUSbsh+DSRCGj3EmSxWhNSKl6XaNDqnHDEasWL+53+gDOnfOqd6g9ZLRTH0GAOluXp30CAwEAAaOCAc8wggHLMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegEBMB0GA1UdDgQWBBQ+Mn5q632bCwAvc0Uba6BoyVn4/TCBggYIKwYBBQUHAQMEdjB0MFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wFQYIKwYBBQUHCwIwCQYHBACL7EkBATAIBgYEAI5GAQEwHwYDVR0jBBgwFoAUXX0LjhjHdotvRbjsbNXjA9XzNd0wEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkGCCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEFBQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQBe4atVNwGmnBFMPD2ZZklrzic8yyVeraLHfWhEPYBAiXhVwoPC3h9ostUM8Qwp6YeVSJoB9OJZrTVOaTIk9UUBiu/8LidDV1R6tM9OnajPjzatD+UgM+dJhdo08F8f2Eu0P/38TlYGUjSEefGsB0Q0LhvJeq09LmOw9a5IFAo6GZqmAJ9Lil+HabQ730f1WcObzdm7Palf8nBPVi4pKv6ok8BPhMMBMJEb1rKLQu7EBPaRRCWGo61R1tFwbsrsPBAfDCTQ9+LQjqlQk3+YW0uehEUIEmvUjnTqs4IjAE8gh4D2+VVV3FPWoEUXBlGrLFt7ZJ+GsTQN6bmqQ/+2NYiGk/N9J1a9KDc1iQc55/doDtBCENX0rqPgJ79NvKc9Dm/dRekLl8geGRWzpBL5GAu1YDRZG+1tkHOSLbUTbuOOvxnEx+e6W1OOs77ffL1lhkdm4rBJecZL2UH7Cz94fur+cHuJl/CEb4gFIVQgTT4xTS0CK41UjSjqiQ7GaaGTQJFlMGldwUTB5+53RXZjkOpspVgakqw5XalxEJwil+293h3fzkHvF3uoRJ3WIPo+M0cxlSw9zKk3qGWZysbgBjTDcLczh4II5qlktYoq6Cvrg/W9LYXNtPF3zXn0JaGRaBOli46cFwaa1ebbALairo/TtC7jdzXX2bsDJfJZKOtaNw=="; + + @Test + public void getDateOfBirthFromCertificateAttribute_datePresent_returns() throws CertificateException { + X509Certificate certificateWithDob = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + + LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithDob); + + assertThat(dateOfBirthCertificateAttribute, is(notNullValue())); + assertThat(dateOfBirthCertificateAttribute, is(LocalDate.of(1903, 3, 3))); + } + + @Test + public void getDateOfBirthFromCertificateAttribute_dateNotPresent_returnsEmpty() throws CertificateException { + X509Certificate certificateWithoutDobAttribute = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); + + LocalDate dateOfBirthCertificateAttribute = CertificateAttributeUtil.getDateOfBirth(certificateWithoutDobAttribute); + + assertThat(dateOfBirthCertificateAttribute, is(nullValue())); + } + + @ParameterizedTest + @ArgumentsSource(AttributeArgumentProvider.class) + void getAttributeValue(ASN1ObjectIdentifier attribute, String expectedValue) throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, attribute); + + assertTrue(attributeValue.isPresent()); + assertThat(attributeValue.get(), is(expectedValue)); + } + + @Test + void getAttributeValue_valueDoesNotExist_returnEmptyOptional() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_WITH_DOB); + String distinguishedName = certificate.getSubjectX500Principal().getName(); + + Optional attributeValue = CertificateAttributeUtil.getAttributeValue(distinguishedName, BCStyle.GENDER); + + assertTrue(attributeValue.isEmpty()); + } + + @Test + void getCertificatePolicy_certificatePolicyIsNotPresent_returnEmptySet() { + X509Certificate certificate = InvalidCertificateGenerator.builder().createCertificate(); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertTrue(certificatePolicy.isEmpty()); + } + + @Test + void getCertificatePolicy_certificatePolicyPresent() throws CertificateException { + X509Certificate certificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV); + + Set certificatePolicy = CertificateAttributeUtil.getCertificatePolicy(certificate); + + assertThat(certificatePolicy, contains("1.3.6.1.4.1.10015.3.17.2", "0.4.0.2042.1.1")); + } + + @Test + void hasNonRepudiation_KeyUsageExtensionIsMissing() { + X509Certificate certificate = InvalidCertificateGenerator.builder() + .withKeyUsage(null) + .createCertificate(); + + assertFalse(CertificateAttributeUtil.hasNonRepudiationKeyUsage(certificate)); + } + + private static class AttributeArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of(Named.of("Given name", BCStyle.GIVENNAME), "BOD"), + Arguments.of(Named.of("Surname", BCStyle.SURNAME), "TESTNUMBER"), + Arguments.of(Named.of("Serial number", BCStyle.SERIALNUMBER), "PNOLV-329999-99901"), + Arguments.of(Named.of("Country", BCStyle.C), "LV") + ); + } + } +} diff --git a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java index 6c174734..47e53d1f 100644 --- a/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java +++ b/src/test/java/ee/sk/smartid/util/NationalIdentityNumberUtilTest.java @@ -1,141 +1,141 @@ -package ee.sk.smartid.util; - -/*- - * #%L - * Smart ID sample Java client - * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS - * %% - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * #L% - */ - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import ee.sk.smartid.AuthenticationIdentity; -import ee.sk.smartid.AuthenticationIdentityMapper; -import ee.sk.smartid.CertificateUtil; -import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; - -public class NationalIdentityNumberUtilTest { - - private static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; - private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; - private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; - - @Test - public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { - X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1801, 1, 1))); - } - - @Test - public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { - X509Certificate lvCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1903, 4, 3))); - } - - @Test - public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { - X509Certificate ltCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); - - AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); - - LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); - - assertThat(dateOfBirth, is(notNullValue())); - assertThat(dateOfBirth, is(LocalDate.of(1960, 9, 6))); - } - - @ParameterizedTest - @ValueSource(strings = {"321205-1234", "331205-1234", "341205-1234", "351205-1234", "361205-1234", "371205-1234", "381205-1234", "391205-1234"}) - public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull(String lvNationalIdentityNumber) { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth(lvNationalIdentityNumber); - assertThat(birthDate, is(nullValue())); - } - - @Test - public void parseLvDateOfBirth_21century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131205-2234"); - assertThat(birthDate, is(LocalDate.of(2005, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_20century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-1234"); - assertThat(birthDate, is(LocalDate.of(1965, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_19century() { - LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-0234"); - assertThat(birthDate, is(LocalDate.of(1865, 12, 13))); - } - - @Test - public void parseLvDateOfBirth_invalidMonth_throwsException() { - var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, - () -> NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234")); - - assertThat(unprocessableSmartIdResponseException.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); - } - - @Test - public void getDateOfBirthFromIdCode_sweden_returnsNull() { - AuthenticationIdentity identity = new AuthenticationIdentity(); - identity.setCountry("SE"); - identity.setIdentityNumber("1995012-79039"); - - assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); - } - - @Test - public void getDateOfBirthFromIdCode_poland_returnsNull() { - AuthenticationIdentity identity = new AuthenticationIdentity(); - identity.setCountry("PL"); - identity.setIdentityNumber("64120301283"); - - assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); - } - -} +package ee.sk.smartid.util; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2025 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import ee.sk.smartid.AuthenticationIdentity; +import ee.sk.smartid.AuthenticationIdentityMapper; +import ee.sk.smartid.CertificateUtil; +import ee.sk.smartid.exception.UnprocessableSmartIdResponseException; + +public class NationalIdentityNumberUtilTest { + + private static final String AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903 = "MIIIhTCCBm2gAwIBAgIQd8HszDVDiJBgRUH8bND/GzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwHhcNMjEwMzA3MjExMzMyWhcNMjQwMzA3MjExMzMyWjCBgzELMAkGA1UEBhMCTFYxLzAtBgNVBAMMJlRFU1ROVU1CRVIsV1JPTkdfVkMsUE5PTFYtMDMwNDAzLTEwMDc1MRMwEQYDVQQEDApURVNUTlVNQkVSMREwDwYDVQQqDAhXUk9OR19WQzEbMBkGA1UEBRMSUE5PTFYtMDMwNDAzLTEwMDc1MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjC6yZx8T1M56IHYCOsOnYhZwtaPP/z4+2A8XDsRz03qj8+80iHxRI4A6+8tIZdEq58QDbpN+BHRE4RHhsdz7RVZJQ9Gxp3dGutJAjxSONBbwzCzmo9fyy+svVBIFZAUbKAZWI6PzDHIztkMJNRONb6DachdX3L0gIGGxFUlbL/DJIhRjAmOG8rJht/bCHwFv0uBrUAGSvJ3AHgokouvwREThM/gvKlijhaPXxACTpignu1jETYJieVC8JS6E2YU+1nca+TCMNa65/KNLjF4Pd+QchLQtJbxEPzsdnHIkwh5SVGegAxpVk/My/9WbL1v08PnivyCARu6/Bc+KX0SERg93+IMrKC+dbkiULMMOWxCXV1LjarFhS0FgQCzdueS96lpMrwfb2ctQRlhRIaP7yOh2IEoHP4diQgzvpVsIywH8oN+lrXtciR8ufhFhsklIRa21iO+PuTY6B+LVpAyZAQFEISUkXOqnzBopFd8OJqyu5z7S7V+axNSeHhyTIXG1Ys+HwGc+w/DBu5KhOONNgmNCeXF6d3ACuMFF6K07ghouBk5fC27Fsgl6D7u2niawgb5ouGXvHq4a756swJphZq63diHE+vBqQHCzdnneVVhiWCwc8bqtNf6ueZtv6hIgzPrFt707IrGbPQ7LvYGmNI/Me7567fzaBNEaykBw/YWqyDV1S3tFKIjKcD/5NGGBDqbHNK1r4Ozob5xJQHpptiYvreQNlPPeTc6aSChS1AK5LTbxrLxifZSh9TOO8IklXdNS6Q4b7th23KhNmU0QGuGva7/JHexfLUuknBr92b8ink4zeZsoe69SI2xW/ta/ANVl4FN2LhJqgyplskNkUCwFadplcKs3+m5gBggz7kh8cLhcaobfHRHh0ogz5kxM95smrk+tFm/oEKV7VkUT9A5ky8Fvei6MtqZ/SmrIiv4Sdlj71U8laGZmZtR7Kgrpu2KMlZROAZdcvvq/ASbhSVfoebUAj+knvds2wOnC9N8MZU8O46UkKwupiyr/KPexAgMBAAGjggINMIICCTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDBVBgNVHSAETjBMMD8GCisGAQQBzh8DEQIwMTAvBggrBgEFBQcCARYjaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQUCLo2Ioa+lsHpd4UfpJLRTrs2CjQwgaMGCCsGAQUFBwEDBIGWMIGTMAgGBgQAjkYBATAVBggrBgEFBQcLAjAJBgcEAIvsSQEBMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8vc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCRU4wCAYGBACORgEEMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYdaHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMzA0MDMtMTAwNzUtWkg0TS1RMA0GCSqGSIb3DQEBCwUAA4ICAQDli94AjzgMUTdjyRzZpOUQg3CljwlMlAKm8jeVDBEL6iQiZuCjc+3BzTbBJU7S8Ye9JVheTaSRJm7HqsSWzm1CYPkJkP9xlqRD9aig57FDgL9MXCWNqUlUf2qtoYEUudW9JgR7eNuLfdOFnUEt4qJm3/F/+emIFnf7xWrS2yaMiRwliA3mJxffh33GRVsEO/w5W4LHpU1v/Pbkuu5hyUGw5IybV9odHTF+JnAPsElBjY9OhB8q+5iwAt++8Udvc1gS4vBIvJzRFrl8XA56AJjl061sm436imAYsy4J6QCz8bdu04tcSJyO+c/sDqDNHjXztFLR8TIqV/amkvP+acavSWULy2NxPDtmD4Pn3T3ycQfeT1HkwZGn3HogLbwqfBbLTWYzNjIfQZthox51IrCSDXbvL9AL3zllFGMcnnc6UkZ4k4+M3WsYD6cnpTl/YZ0R9spc8yQ+Vgj58Iq7yyzY/Uf1OkS0GCTBPtfToKmEXUFwKma/pcmsHx5aV7Pm2Lo+FiTrVw0lgB+t0qGlqT52j4H7KrvQi0xDuEapqbR3AAPZuiT8+S6Q9Oyq70kS0CG9vZ0f6q3Pz1DfCG8hUcjwzaf5McWMQLSdQK5RKkimDW71Ir2AmSTRNvm0A3IbhuEX2JVN0UGBhV5oIy8ypaC9/3XSnS4ZeQCF9WbA2IOmyw=="; + private static final String AUTH_CERTIFICATE_EE = "MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlELVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcwFQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQTk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTALBgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEwDQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnUhKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6zlzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpedy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0UaE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0wLTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2nT5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339zt7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKxKegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XKygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1UdDgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XMC2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4wKQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsGAQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNLXzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifmrjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2pKmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6vZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3FaYpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8Dj/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5oPEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql440sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytfq8s5bZci5vnHm110lnPhQxM="; + private static final String AUTH_CERTIFICATE_LT = "MIIHdjCCBV6gAwIBAgIQMBAfDpK5mvZbxKkN2GdiUzANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDDB9Ob3J0YWwgTlFTSzE2IFRlc3QgQ2VydCBTaWduaW5nMB4XDTE4MTAxNTE0NDk0OVoXDTIzMTAxNDIwNTk1OVowgb8xCzAJBgNVBAYTAkxUMU0wSwYDVQQDDERTVVJOQU1FUE5PTFQtMzYwMDkwNjc5NjgsRk9SRU5BTUVQTk9MVC0zNjAwOTA2Nzk2OCxQTk9MVC0zNjAwOTA2Nzk2ODEhMB8GA1UEBAwYU1VSTkFNRVBOT0xULTM2MDA5MDY3OTY4MSIwIAYDVQQqDBlGT1JFTkFNRVBOT0xULTM2MDA5MDY3OTY4MRowGAYDVQQFExFQTk9MVC0zNjAwOTA2Nzk2ODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIHhkVlQIBdyiyDplUOlqUQs8mL4+XOwIVXP1LqoQd1bOpNm33jBOX6k+hAtfSK1gLr3AlahKKVhSEjLh3hwJxFS/fL/jYhOH5ZQdO8gQVKofMPSB/O3opal+ybfKFaWcfqtu9idpDWxRoIwVMJMpVvd1kWYWT2hpJclECASrPNeynqpgcoFqM9GcW0KvgGfNOOZ1dz8PhN3VlSNY2z3tTnWZavqo8e2omnipxg6cjrL7BZ73ooBoyfg8E8jJDywXa7VIxfcaSaW54AUuYS55rVuX5sXAeOg2OWVsO9829JGjPUiEgH1oyh03Gsi4QlSJ5LBmGwC9D4/yg94FYihcUoprUbSOGOtXVGBAK3ZDU5SLYec9VMpNngAXa/MlLov9ePv4ZswJFs59FGkTNPOLVO/40sdwUn3JWwpkAngTKgQ+Kg5yr6+WTR2e3eCKS2vGqduFfLfDuI0Ywaz0y/NmtTwMU9o8JQ0rijTILPd0CvRlnPXNrGeH4x3WYCfb3JAk+hI1GCyLTg1TBkWH3CCpnLTsejGK1iJwsEzvE2rxWzi3yUXN9HhuQfg4pxe7YoFH5rY/cguIUqRSRQ072igENBgEraAkRMby/qci8Iha9lGf2BQr8fjCBqA5ywSxdwpI/l8n/eB343KqpnWu8MM+p7Hh6XllT5sX2ZyYy292hSxAgMBAAGjggIAMIIB/DAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQEwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4EFgQUuRyFPVIigHbTJXCo+Py9PoSOYCgwgYIGCCsGAQUFBwEDBHYwdDBRBgYEAI5GAQUwRzBFFj9odHRwczovL3NrLmVlL2VuL3JlcG9zaXRvcnkvY29uZGl0aW9ucy1mb3ItdXNlLW9mLWNlcnRpZmljYXRlcy8TAkVOMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMB8GA1UdIwQYMBaAFOxFjsHgWFH8xUhlnCEfJfUZWWG9MBMGA1UdJQQMMAoGCCsGAQUFBwMCMHYGCCsGAQUFBwEBBGowaDAjBggrBgEFBQcwAYYXaHR0cDovL2FpYS5zay5lZS9ucTIwMTYwQQYIKwYBBQUHMAKGNWh0dHBzOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfTlEtU0tfMjAxNi5kZXIuY3J0MDYGA1UdEQQvMC2kKzApMScwJQYDVQQDDB5QTk9MVC0zNjAwOTA2Nzk2OC01MkJFNEE3NC0zNkEwDQYJKoZIhvcNAQELBQADggIBAKhoKClb4b7//r63rTZ/91Jya3LN60pJY4Qe5/nfg3zapbIuGpWzZt6ZkPPrdlGoS1GPyfP9CCX79F4keUi9aFnRquYJ09T3Bmq37eGEsHtwG27Nxl+/ysj7Z7B80B6icn1aGFSNCd+0IHIJslLKhWYI0/dKJjck0iGTfD4iHF31aEvjHdo+Xt2ond1SVHMYT35dQ16GKDtd5idq2bjVJPJmM6vD+21GrZcct83vIKCxx6re/JcHcQudQlMnMR0pL/KOtdSl/4e3TcdXsvubm8fi3sFnfYsaRoTMJPjICEEuBMziiHIsLQCzetVArCuEzej39fqJxYGsanfpcLZxjc9oVmVpFOhzyg5O5NyhrIA8ErXs0gqgMnVPGv56u0R1/Pw8ZeYo7GrkszJpFR5N8vPGpWXUGiPMhnkeqFNZ4Gjzt3GOLiVJ9XWKLzdNJwF+3en0f1D35qSjEj65/co52SAaopGy24uKBfndHIQVPftUhPMOPwcQ7fo1Btq7dRt0OGBbLmcZmdMBASQWQKFohJDUnk6UHEfjCmCO9c1tVrk5Jj9wXhmxBKSXnQMi8NR+HbYy+wJATzKUUm4sva1euygDwS0eMLtSAaNpwdFKH8WLk9tiRkU9kukGNZyQgnr5iOH8ALpOiXSQ8pVHw1qgNdr7g/Si3r/NQpMQQm/+IP5p"; + + @Test + public void getDateOfBirthFromIdCode_estonianIdCode_returns() throws CertificateException { + X509Certificate eeCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_EE); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(eeCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1801, 1, 1))); + } + + @Test + public void getDateOfBirthFromIdCode_latvianIdCode_returns() throws CertificateException { + X509Certificate lvCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LV_DOB_03_APRIL_1903); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(lvCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1903, 4, 3))); + } + + @Test + public void getDateOfBirthFromIdCode_lithuanianIdCode_returns() throws CertificateException { + X509Certificate ltCertificate = CertificateUtil.toX509CertificateFromEncodedString(AUTH_CERTIFICATE_LT); + + AuthenticationIdentity identity = AuthenticationIdentityMapper.from(ltCertificate); + + LocalDate dateOfBirth = NationalIdentityNumberUtil.getDateOfBirth(identity); + + assertThat(dateOfBirth, is(notNullValue())); + assertThat(dateOfBirth, is(LocalDate.of(1960, 9, 6))); + } + + @ParameterizedTest + @ValueSource(strings = {"321205-1234", "331205-1234", "341205-1234", "351205-1234", "361205-1234", "371205-1234", "381205-1234", "391205-1234"}) + public void parseLvDateOfBirth_withoutDateOfBirth_returnsNull(String lvNationalIdentityNumber) { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth(lvNationalIdentityNumber); + assertThat(birthDate, is(nullValue())); + } + + @Test + public void parseLvDateOfBirth_21century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131205-2234"); + assertThat(birthDate, is(LocalDate.of(2005, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_20century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-1234"); + assertThat(birthDate, is(LocalDate.of(1965, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_19century() { + LocalDate birthDate = NationalIdentityNumberUtil.parseLvDateOfBirth("131265-0234"); + assertThat(birthDate, is(LocalDate.of(1865, 12, 13))); + } + + @Test + public void parseLvDateOfBirth_invalidMonth_throwsException() { + var unprocessableSmartIdResponseException = assertThrows(UnprocessableSmartIdResponseException.class, + () -> NationalIdentityNumberUtil.parseLvDateOfBirth("131365-1234")); + + assertThat(unprocessableSmartIdResponseException.getMessage(), is("Unable get birthdate from Latvian personal code 131365-1234")); + } + + @Test + public void getDateOfBirthFromIdCode_sweden_returnsNull() { + AuthenticationIdentity identity = new AuthenticationIdentity(); + identity.setCountry("SE"); + identity.setIdentityNumber("1995012-79039"); + + assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); + } + + @Test + public void getDateOfBirthFromIdCode_poland_returnsNull() { + AuthenticationIdentity identity = new AuthenticationIdentity(); + identity.setCountry("PL"); + identity.setIdentityNumber("64120301283"); + + assertThat(NationalIdentityNumberUtil.getDateOfBirth(identity), is(nullValue())); + } + +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 34e98e4b..c05b1e63 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -1,15 +1,15 @@ - - - - - %d{dd.MM.yyyy HH:mm:ss.SSS} %-5p [%thread] [%logger{36}.%method:%line] - %m%n - - - - - - - - - - + + + + + %d{dd.MM.yyyy HH:mm:ss.SSS} %-5p [%thread] [%logger{36}.%method:%line] - %m%n + + + + + + + + + + diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json index fa1ecb06..58621ea6 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-invalid-request.json @@ -1,7 +1,7 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json index 29a54242..0ccbe990 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-qr-code.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json index 9d5e2ad7..2f6ed5ce 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "certificateLevel": "QUALIFIED", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", - "requestProperties": { - "shareMdClientIpAddress": true - }, - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "certificateLevel": "QUALIFIED", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json index 9712dd01..4a0177cb 100644 --- a/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json +++ b/src/test/resources/requests/auth/device-link/device-link-authentication-session-request-same-device-only-required-fields.json @@ -1,15 +1,15 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "certificateLevel": "QUALIFIED", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "certificateLevel": "QUALIFIED", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbj8ifV0=", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json index 8adf3305..683dbb84 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", - "requestProperties": { - "shareMdClientIpAddress": true - }, - "vcType": "numeric4" -} +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "requestProperties": { + "shareMdClientIpAddress": true + }, + "vcType": "numeric4" +} diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json index a9dbf465..22276dab 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-invalid.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" -} +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" +} diff --git a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json index 7efc8494..5fd36502 100644 --- a/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json +++ b/src/test/resources/requests/auth/notification/notification-authentication-session-request-only-required-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "ACSP_V2", - "signatureProtocolParameters": { - "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", - "vcType": "numeric4" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "ACSP_V2", + "signatureProtocolParameters": { + "rpChallenge": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvZ2luPyJ9XQ==", + "vcType": "numeric4" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json index ee280a89..f1d018f4 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-all-fields.json @@ -1,5 +1,5 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "ADVANCED" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json index b006f4df..39c99d9e 100644 --- a/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json +++ b/src/test/resources/requests/sign/certificate-by-document-number-request-only-required-fields.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json index 9653b796..e19b9623 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-all-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-0000-0000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json index 5732ba95..b980efff 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-qr-code.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json index 5caee0c0..00ceb1cb 100644 --- a/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json +++ b/src/test/resources/requests/sign/device-link/signature/device-link-signature-request-same-device.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24gZG9jdW1lbnQ/In1d", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json index 3da1f40c..16cf47ee 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-all-fields.json @@ -1,10 +1,10 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "initialCallbackUrl": "https://example.com/callback", - "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback", + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json index b06bd4e5..4c8d933b 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-device-link.json @@ -1,6 +1,6 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "initialCallbackUrl": "https://example.com/callback" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "initialCallbackUrl": "https://example.com/callback" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json index ee280a89..f1d018f4 100644 --- a/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json +++ b/src/test/resources/requests/sign/linked/cert-choice/certificate-choice-session-request-for-qr-code.json @@ -1,5 +1,5 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "ADVANCED" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "ADVANCED" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json index c233c758..7c17de75 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-all-fields.json @@ -1,19 +1,19 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "linkedSessionID": "10000000-0000-000-000-000000000000", - "nonce": "cmFuZG9tTm9uY2U=", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "nonce": "cmFuZG9tTm9uY2U=", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json index 7fcdc646..0b2cc34d 100644 --- a/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/linked/signature/linked-notification-signature-session-request-only-required-fields.json @@ -1,14 +1,14 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "linkedSessionID": "10000000-0000-000-000-000000000000", - "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "2xN/gwSxWos+lJPQ/AeIlBXmdPRRlOfOD5+Ezz0FWWABd96mQNkR/b1/2wpAIGwS1SsW1oRVtdRVKYyV21yGWA==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "linkedSessionID": "10000000-0000-000-000-000000000000", + "interactions": "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNpZ24/In1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json index 7862df88..f44f8d16 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-all-fields.json @@ -1,9 +1,9 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QUALIFIED", - "nonce": "cmFuZG9tTm9uY2U=", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QUALIFIED", + "nonce": "cmFuZG9tTm9uY2U=", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json index 8ed2cc2f..5b1cac06 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid-credentials.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "NOT DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json index 92c7a7ef..c9b762c0 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-invalid.json @@ -1,3 +1,3 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json index b006f4df..39c99d9e 100644 --- a/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/notification/cert-choice/certificate-choice-session-request-only-required-fields.json @@ -1,4 +1,4 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json index 6028bfe7..fea28091 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-all-fields.json @@ -1,18 +1,18 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "certificateLevel": "QSCD", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d", - "requestProperties": { - "shareMdClientIpAddress": true - } +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "certificateLevel": "QSCD", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "nonce": "d8XkbEnA0WsE0PvBZZoxGnPI4ml9qk", + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d", + "requestProperties": { + "shareMdClientIpAddress": true + } } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json index 272ed234..5a0ac32e 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid-credentials.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "NOT DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "NOT DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json index 92c7a7ef..c9b762c0 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-invalid.json @@ -1,3 +1,3 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000" } \ No newline at end of file diff --git a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json index 0f62fd1b..5d0e25f1 100644 --- a/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json +++ b/src/test/resources/requests/sign/notification/signature/notification-signature-session-request-only-required-fields.json @@ -1,13 +1,13 @@ -{ - "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", - "relyingPartyName": "DEMO", - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signatureProtocolParameters": { - "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512" - } - }, - "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" +{ + "relyingPartyUUID": "00000000-0000-4000-8000-000000000000", + "relyingPartyName": "DEMO", + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signatureProtocolParameters": { + "digest": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512" + } + }, + "interactions": "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IlNpZ24gaXQhIn1d" } \ No newline at end of file diff --git a/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json index d4cc540f..79fb9fdb 100644 --- a/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json +++ b/src/test/resources/responses/auth/device-link/device-link-authentication-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "sessionToken": "sessionToken", - "sessionSecret": "c2Vzc2lvblNlY3JldA==", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "sessionToken": "sessionToken", + "sessionSecret": "c2Vzc2lvblNlY3JldA==", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/auth/notification/notification-session-response.json b/src/test/resources/responses/auth/notification/notification-session-response.json index 836fc071..6b21efb5 100644 --- a/src/test/resources/responses/auth/notification/notification-session-response.json +++ b/src/test/resources/responses/auth/notification/notification-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json index 53c69870..5491eb3e 100644 --- a/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json +++ b/src/test/resources/responses/certificate-by-document-number-response-unknown-state.json @@ -1,9 +1,9 @@ -{ - "state": "UNKNOWN", - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "UNKNOWN", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/certificate-by-document-number-response.json b/src/test/resources/responses/certificate-by-document-number-response.json index 49886071..78e18794 100644 --- a/src/test/resources/responses/certificate-by-document-number-response.json +++ b/src/test/resources/responses/certificate-by-document-number-response.json @@ -1,9 +1,9 @@ -{ - "state": "OK", - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "OK", + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-account-unusable.json b/src/test/resources/responses/session-status-account-unusable.json index 247caa53..9cf51132 100644 --- a/src/test/resources/responses/session-status-account-unusable.json +++ b/src/test/resources/responses/session-status-account-unusable.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "ACCOUNT_UNUSABLE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "ACCOUNT_UNUSABLE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-document-unusable.json b/src/test/resources/responses/session-status-document-unusable.json index 69d2dfa8..82f93325 100644 --- a/src/test/resources/responses/session-status-document-unusable.json +++ b/src/test/resources/responses/session-status-document-unusable.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "DOCUMENT_UNUSABLE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "DOCUMENT_UNUSABLE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-expected-linked-session.json b/src/test/resources/responses/session-status-expected-linked-session.json index 66c88f0e..ea5b572f 100644 --- a/src/test/resources/responses/session-status-expected-linked-session.json +++ b/src/test/resources/responses/session-status-expected-linked-session.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "EXPECTED_LINKED_SESSION", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "EXPECTED_LINKED_SESSION", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-protocol-failure.json b/src/test/resources/responses/session-status-protocol-failure.json index c3c4c5d0..77efaa4d 100644 --- a/src/test/resources/responses/session-status-protocol-failure.json +++ b/src/test/resources/responses/session-status-protocol-failure.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "PROTOCOL_FAILURE", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "PROTOCOL_FAILURE", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-running-with-ignored-properties.json b/src/test/resources/responses/session-status-running-with-ignored-properties.json index a9e2800f..b44c1615 100644 --- a/src/test/resources/responses/session-status-running-with-ignored-properties.json +++ b/src/test/resources/responses/session-status-running-with-ignored-properties.json @@ -1,5 +1,5 @@ -{ - "state": "RUNNING", - "ignoredProperties": ["testingIgnored", "testingIgnoredTwo"], - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "RUNNING", + "ignoredProperties": ["testingIgnored", "testingIgnoredTwo"], + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-running.json b/src/test/resources/responses/session-status-running.json index b7ec24cb..221b89f7 100644 --- a/src/test/resources/responses/session-status-running.json +++ b/src/test/resources/responses/session-status-running.json @@ -1,4 +1,4 @@ -{ - "state": "RUNNING", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "RUNNING", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-server-error.json b/src/test/resources/responses/session-status-server-error.json index a914c4f5..d9e2716a 100644 --- a/src/test/resources/responses/session-status-server-error.json +++ b/src/test/resources/responses/session-status-server-error.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "SERVER_ERROR", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "SERVER_ERROR", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-authentication.json b/src/test/resources/responses/session-status-successful-authentication.json index 62fdd536..bdda60ba 100644 --- a/src/test/resources/responses/session-status-successful-authentication.json +++ b/src/test/resources/responses/session-status-successful-authentication.json @@ -1,38 +1,38 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "signatureProtocol": "ACSP_V2", - "signature": { - "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", - "serverRandom": "J0iyCYOu8cTWuoD8rD05IIrZ", - "userChallenge": "GnsWXXEjTCKR89fj9uo5u5ReBZ9JR7_pezLAI5jMS00", - "flowType": "QR", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA3-512", - "maskGenAlgorithm": { - "algorithm": "id-mgf1", - "parameters": { - "hashAlgorithm": "SHA3-512", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "saltLength": 64, - "trailerField": "0xbc", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQybEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7CXahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEOTnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5nzCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcyhPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29JcowTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9PsmN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1JgxBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRjFisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QACBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v09hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSgAt+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0GCysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rlc3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHKmTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0XnPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxWCbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2", - "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "interactionTypeUsed": "displayTextAndPIN", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "signatureProtocol": "ACSP_V2", + "signature": { + "value": "TstqDys5iuxUk/HZqxwTZH95ynaBF3GK8HziQlo//ujbQQTdN8e0bU1a9E7lQmBZvKxNI1FtW47MFkwYS0H12u7TNYcmrmGexCRmarjl88tPq7xSw2yUUWawy2dKtBhMlVYtKHz+cr33Jqngm6O4birSUL0tMjENixBu/tCfN6j+6FO/1i0moVSSw1Aj1E5fHa/c8uFuta83lIDlAbUOJi1kjaF5NOeY4hMgb2/K5CCRkgjf6tSCGhFQCceIduBp3CPt7Ch1ze7aCMnaR1aIadyRzMVM995paQ4EihYfqRbuiJ0Izueanp9rTJPx5tqD/SOwIrTkwd7EcEnhaK13zj6u4p+EtbNuTAY6zioT1BvgIRIRr1HF7htrggFjpgkPBRkpE1SQG6jIGr8rlgkS1yTqtOi0rdkKx9l7sIfLIeC2G14YR1yIK4NJPoJWHu+/PQ13UVi1c53uxSWc7eSCey7QlYEwRQQcFN7I8V1ahaRchMNtLGdswi9s1c02hFsqmX4/jLh2MyND1sm+Go0dpPR1H3SPrOwTxon62AvGooWVvQbAMUAw3pYkT5s+4ECBczGxIbIYcPGSky+luj02Wf1Ux20ZQdj0pR8i789JC3Vd9x7/4J+ylwsFlKqlMvS2V/hKph1+vCqG58Urv7KWPDK+Y69vyeoFqYaWBIUOOB2F6L6388CxtFN37bB5qMyMaFYfjScIMN9O8DxDQ1bJI8kadIrzzvgqAA1N/ptcWuHOvH1MK1lZlQH4YjjkzpU/o/Y0AaZpr+jTRHMf+43fqF8tL96FG0yze5372yRxkLJjWizEXhKZpcE58oVEVKTITwWLBMb76zJzCoVFa495x6WqLH6gkiJphNFARaUX11zxnH++U5Yvn37Gc3WVHGNCkVSDFTjMZt2reG982SwxV0OH3ZiMzml8XHfQOLccIXdR0OycPYrqNWY8jZMn57npksSRTQtnfxzxMo227mlR0uk02f62VwxZiE3oj4T3SqEr24hep5+1lWMVtB1/Lf1N", + "serverRandom": "J0iyCYOu8cTWuoD8rD05IIrZ", + "userChallenge": "GnsWXXEjTCKR89fj9uo5u5ReBZ9JR7_pezLAI5jMS00", + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA3-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA3-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQybEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7CXahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEOTnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5nzCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcyhPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29JcowTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9PsmN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1JgxBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRjFisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QACBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v09hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSgAt+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYIKwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJRC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5zay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQwNTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsGAQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0GCysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rlc3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHKmTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0XnPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxWCbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2", + "certificateLevel": "QUALIFIED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "interactionTypeUsed": "displayTextAndPIN", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-successful-certificate-choice.json b/src/test/resources/responses/session-status-successful-certificate-choice.json index 70040c89..df74f5e2 100644 --- a/src/test/resources/responses/session-status-successful-certificate-choice.json +++ b/src/test/resources/responses/session-status-successful-certificate-choice.json @@ -1,14 +1,14 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField06744": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/session-status-successful-signature.json b/src/test/resources/responses/session-status-successful-signature.json index 4de6f326..f5372743 100644 --- a/src/test/resources/responses/session-status-successful-signature.json +++ b/src/test/resources/responses/session-status-successful-signature.json @@ -1,37 +1,37 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "OK", - "documentNumber": "PNOEE-40504040001-MOCK-Q", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "signatureProtocol": "RAW_DIGEST_SIGNATURE", - "signature": { - "value": "fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgLLScB4+0qEhji9HKNRNHpXsip6LmoDiWD7pBlBPL0YOsFczSEpRpCe3NLxWWCWzd7i6tFcYXFwhpXEaUtoUhpstGOtjYGHkvXzMcQmiyXC4qWrw3RrSqEnB2ONmuZE60brwyRne8xYgBMmHcvu0s9jcTDWM+ppNfjm4WED+u5sOTGbSyO7Eg9kOhfDenYg++1Cg6zlpWwd9OMpojmK2pOsZC0JmcOIyQ+Cf2mBobx0qt6cPot9/bx1X5uTualJfxMrRZSE3twuXq0f3f0A+Yv3kHhx/AdzQaAuydtIdlz60naWIS84PUnAeOKiYLRbRRawLc4MGZHqn4DeFHI4zvzMLhz13O8pirFWb7qWJ+RvsgyAMTHmAwzPmtpwYT90z22Bc915qTufaJ48/m8DXGARQdbOP+/+5a4Q7PwnrdAm7SwbnNcAlvzVQO+o1onhnPKGz79EYVIgNj+9Hijqdggw41lBEjl82Lr7LNuVhz2wVaBYD4yELzmoDEOW69wWMQ6WHwK/SF1Xe44ENi6JSZE1f19AQT0+xOt0FWKloQ9Tn/kvtw+/LhLzugOtf61t9HBLCt73iSpJ6SqD4lMHxozJ5SEJNm05DBhaCf3IlZzw0HYFRMZNUXx/7y2QhOWpRMFZIhjHjFedi1IxPj3BmKTL1Vgq5koCDxF1Wbdl+UONK9UthYpKpU13Wi04YubYLb3VKw9wb9f9YlweXoeUHxOTy3l6f+Z6lP3EYAp7NbyJlPCW7yhTeS4kg4uzftqr+2cW4ORdQvs2Va7qrkdu5sd8d72jKWuQluviR5gCTLvQtttc/Tex/ix8iuQ4ffHTap+gnrcEgIA3Th8Z0m93kwpE+YLjHAxMQmzgkR/iPoDpTutpqjoLbrhLgUQpSJ5pYyRQgc6iM/BN6+xpe2GFBoODXzBj81OK1qDN89A26ldyLDan0tkSKIuVJWIapDxQick", - "flowType": "QR", - "signatureAlgorithm": "rsassa-pss", - "signatureAlgorithmParameters": { - "hashAlgorithm": "SHA-512", - "maskGenAlgorithm": { - "algorithm": "id-mgf1", - "parameters": { - "hashAlgorithm": "SHA-512", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "saltLength": 64, - "trailerField": "0xbc", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "cert": { - "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", - "certificateLevel": "QUALIFIED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "interactionTypeUsed": "verificationCodeChoice", - "deviceIpAddress": "203.0.113.34", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" -} +{ + "state": "COMPLETE", + "result": { + "endResult": "OK", + "documentNumber": "PNOEE-40504040001-MOCK-Q", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "signatureProtocol": "RAW_DIGEST_SIGNATURE", + "signature": { + "value": "fa6riQ8ZXb6esyDpsag9xwupVv5c64jjlvIJ5b+A9g45onozUnd3MMM8S5UYmrgLLScB4+0qEhji9HKNRNHpXsip6LmoDiWD7pBlBPL0YOsFczSEpRpCe3NLxWWCWzd7i6tFcYXFwhpXEaUtoUhpstGOtjYGHkvXzMcQmiyXC4qWrw3RrSqEnB2ONmuZE60brwyRne8xYgBMmHcvu0s9jcTDWM+ppNfjm4WED+u5sOTGbSyO7Eg9kOhfDenYg++1Cg6zlpWwd9OMpojmK2pOsZC0JmcOIyQ+Cf2mBobx0qt6cPot9/bx1X5uTualJfxMrRZSE3twuXq0f3f0A+Yv3kHhx/AdzQaAuydtIdlz60naWIS84PUnAeOKiYLRbRRawLc4MGZHqn4DeFHI4zvzMLhz13O8pirFWb7qWJ+RvsgyAMTHmAwzPmtpwYT90z22Bc915qTufaJ48/m8DXGARQdbOP+/+5a4Q7PwnrdAm7SwbnNcAlvzVQO+o1onhnPKGz79EYVIgNj+9Hijqdggw41lBEjl82Lr7LNuVhz2wVaBYD4yELzmoDEOW69wWMQ6WHwK/SF1Xe44ENi6JSZE1f19AQT0+xOt0FWKloQ9Tn/kvtw+/LhLzugOtf61t9HBLCt73iSpJ6SqD4lMHxozJ5SEJNm05DBhaCf3IlZzw0HYFRMZNUXx/7y2QhOWpRMFZIhjHjFedi1IxPj3BmKTL1Vgq5koCDxF1Wbdl+UONK9UthYpKpU13Wi04YubYLb3VKw9wb9f9YlweXoeUHxOTy3l6f+Z6lP3EYAp7NbyJlPCW7yhTeS4kg4uzftqr+2cW4ORdQvs2Va7qrkdu5sd8d72jKWuQluviR5gCTLvQtttc/Tex/ix8iuQ4ffHTap+gnrcEgIA3Th8Z0m93kwpE+YLjHAxMQmzgkR/iPoDpTutpqjoLbrhLgUQpSJ5pYyRQgc6iM/BN6+xpe2GFBoODXzBj81OK1qDN89A26ldyLDan0tkSKIuVJWIapDxQick", + "flowType": "QR", + "signatureAlgorithm": "rsassa-pss", + "signatureAlgorithmParameters": { + "hashAlgorithm": "SHA-512", + "maskGenAlgorithm": { + "algorithm": "id-mgf1", + "parameters": { + "hashAlgorithm": "SHA-512", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "saltLength": 64, + "trailerField": "0xbc", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "cert": { + "value": "MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSwwKgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBjMQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwKVEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjrawSyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCLr6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8smatmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+WrR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kpRKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXkaaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4ewa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGkpkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTMljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8fjGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVYxH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIICizAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYDVR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUFBwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQAjkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA57Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sCMCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozldCQ==", + "certificateLevel": "QUALIFIED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "interactionTypeUsed": "verificationCodeChoice", + "deviceIpAddress": "203.0.113.34", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +} diff --git a/src/test/resources/responses/session-status-timeout.json b/src/test/resources/responses/session-status-timeout.json index 8dfe0a10..32dae0d6 100644 --- a/src/test/resources/responses/session-status-timeout.json +++ b/src/test/resources/responses/session-status-timeout.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "TIMEOUT", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "TIMEOUT", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-cert-choice.json b/src/test/resources/responses/session-status-user-refused-cert-choice.json index c89ba60e..bd4616a7 100644 --- a/src/test/resources/responses/session-status-user-refused-cert-choice.json +++ b/src/test/resources/responses/session-status-user-refused-cert-choice.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_CERT_CHOICE", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_CERT_CHOICE", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json index 24ce4f44..62656ae6 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation-vc-choice.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "confirmationMessageAndVerificationCodeChoice", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessageAndVerificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-confirmation.json b/src/test/resources/responses/session-status-user-refused-confirmation.json index 9c9507ff..d3b1af6e 100644 --- a/src/test/resources/responses/session-status-user-refused-confirmation.json +++ b/src/test/resources/responses/session-status-user-refused-confirmation.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "confirmationMessage", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "confirmationMessage", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json index 9817103e..be9fecf6 100644 --- a/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json +++ b/src/test/resources/responses/session-status-user-refused-display-text-and-pin.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "displayTextAndPIN", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "displayTextAndPIN", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused-vc-choice.json b/src/test/resources/responses/session-status-user-refused-vc-choice.json index 038c9eef..a06e5f09 100644 --- a/src/test/resources/responses/session-status-user-refused-vc-choice.json +++ b/src/test/resources/responses/session-status-user-refused-vc-choice.json @@ -1,12 +1,12 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED_INTERACTION", - "details": { - "interaction": "verificationCodeChoice", - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED_INTERACTION", + "details": { + "interaction": "verificationCodeChoice", + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField498321": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-user-refused.json b/src/test/resources/responses/session-status-user-refused.json index 1b3d0346..7c3e55eb 100644 --- a/src/test/resources/responses/session-status-user-refused.json +++ b/src/test/resources/responses/session-status-user-refused.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "USER_REFUSED", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "USER_REFUSED", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/session-status-wrong-vc.json b/src/test/resources/responses/session-status-wrong-vc.json index b32bef4c..9dd59fc9 100644 --- a/src/test/resources/responses/session-status-wrong-vc.json +++ b/src/test/resources/responses/session-status-wrong-vc.json @@ -1,8 +1,8 @@ -{ - "state": "COMPLETE", - "result": { - "endResult": "WRONG_VC", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" - }, - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "state": "COMPLETE", + "result": { + "endResult": "WRONG_VC", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" + }, + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json index 2305654b..ae3b7df6 100644 --- a/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json +++ b/src/test/resources/responses/sign/device-link/signature/device-link-signature-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "test-session-id", - "sessionToken": "test-session-token", - "sessionSecret": "c2Vzc2lvblNlY3JldA==", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "test-session-id", + "sessionToken": "test-session-token", + "sessionSecret": "c2Vzc2lvblNlY3JldA==", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json index 739a88d1..3e18ae14 100644 --- a/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json +++ b/src/test/resources/responses/sign/linked/certificate-choice/device-link-certificate-choice-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "sessionToken": "sampleSessionToken", - "sessionSecret": "sampleSessionSecret", - "deviceLinkBase": "https://smart-id.com/device-link/", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "sessionToken": "sampleSessionToken", + "sessionSecret": "sampleSessionSecret", + "deviceLinkBase": "https://smart-id.com/device-link/", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json index 6186cf1b..29b7df71 100644 --- a/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json +++ b/src/test/resources/responses/sign/linked/signature/linked-notification-signature-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json index 6186cf1b..29b7df71 100644 --- a/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json +++ b/src/test/resources/responses/sign/notification/cert-choice/notification-certificate-choice-session-response.json @@ -1,4 +1,4 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "unknownField9433454": "this field is added to verify that client doesn't fail when new fields are added in future" } \ No newline at end of file diff --git a/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json index 1133b2a4..7ba7d5d3 100644 --- a/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json +++ b/src/test/resources/responses/sign/notification/signature/notification-signature-session-response.json @@ -1,7 +1,7 @@ -{ - "sessionID": "00000000-0000-0000-0000-000000000000", - "vc": { - "type": "numeric4", - "value": "0000" - } +{ + "sessionID": "00000000-0000-0000-0000-000000000000", + "vc": { + "type": "numeric4", + "value": "0000" + } } \ No newline at end of file diff --git a/src/test/resources/sid_demo_sk_ee.pem b/src/test/resources/sid_demo_sk_ee.pem index 0dcd5561..7ae6d9c8 100644 --- a/src/test/resources/sid_demo_sk_ee.pem +++ b/src/test/resources/sid_demo_sk_ee.pem @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGxTCCBa2gAwIBAgIQB//0m9ljohCn8LB5KDcE1jANBgkqhkiG9w0BAQsFADBZ -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE -aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQx -MDAzMDAwMDAwWhcNMjUxMDE0MjM1OTU5WjBVMQswCQYDVQQGEwJFRTEQMA4GA1UE -BxMHVGFsbGlubjEbMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQQD -Ew5zaWQuZGVtby5zay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKAyy0yvjRCrATznThIwCu/wPCU5mV5UZIzNWl9KXx+gQiBp92SXfTOokkfiikBH -09HI+yVr3zI2U6FR8Tj21GiFE3bttmpCw8tJLmTe/P0Xah1D6vVkymbBt69N24ur -RqhW9in84WdkPc30vGJ+TdIj3jIePAbK3hHbpm+BfeyUhM48xXRgW+cBA//6R1C9 -lUaF9Ycylf+g/P7FpmzHRk2HF3bPyWziBVOhIADtqMyVEJk20dl0SWGsCmAJuAhM -mOPc87zpXYzlAlY24XgsTyQdDnqmJn8ZukDahIt9ybKH/WPLkZfw6xBnsQKXdG0J -HBqBsgQdPDFsrsY45o4ek0kCAwEAAaOCA4swggOHMB8GA1UdIwQYMBaAFHSFgMBm -x9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBSK7cmy40mto6zFVpcvnOyggb6YnzAZ -BgNVHREEEjAQgg5zaWQuZGVtby5zay5lZTA+BgNVHSAENzA1MDMGBmeBDAECAjAp -MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0P -AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0f -BIGXMIGUMEigRqBEhkJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH -bG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9j -cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAy -MENBMS0xLmNybDCBhwYIKwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8v -b2NzcC5kaWdpY2VydC5jb20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRp -Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0x -LmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdwAS -8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZJR+i+zAAAEAwBIMEYC -IQC7tPwb72Mur1ljtCP8g1/BkS6nJV0QeueW3eSa2L+PkwIhAPCJOyx++Vg5mE5D -6S0ctqbVRQsM5XGKYrBzAyzh0QHaAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65 -Ay/ZDowuebgAAAGSUfovdQAABAMARzBFAiEA6ifcmc/Si0vOqT4JTAMqervuE7Uz -iYGZIIZI09BYINICICeJuQZrqP7aHqn9+0iyvl5ptJl2cZ5YyqF3Km9f6vu4AHYA -5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6hzId/R43jlAAAAGSUfovjAAABAMARzBF -AiEAkdK3dAY6ABFtaE1bTjIlYAF5cFT8N2pvxL0mA79LlDwCIFGZJ3EYJfxVbj9m -S/8FynieG/02iMF6xzmmrU58La0pMA0GCSqGSIb3DQEBCwUAA4IBAQCnq3OnD4uw -uvt75qYIBgFNN+nIMslacl8iQYSOswr+K90QzL/yf+lLafDX0QMtDL5b2t1a834R -8efjlEuISfp+YjTdtnNV1jZ7nnkHcFMP1MGbv/JQigPO8AgL+oxGHiRCp6FNJTwt -FtvHkqd5rDJUU988LdND4aYtmKYmGKj06sSqhpl9xmbIxdXPvaJGoHC/gEpM8AKw -oL4afke2q3FpjQ1eDT+37pjsEjQi6nT0/cSNoyxy4QbqWBgGclmb9ZAfOFkaO5U3 -bhRopdPzRSrQROUF0ovPk4aC+b74KAV/oxtQjPTdpdxTVBwjfn2tpes5q+TZUGSZ -AyP23gCAvmuj ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGxTCCBa2gAwIBAgIQB//0m9ljohCn8LB5KDcE1jANBgkqhkiG9w0BAQsFADBZ +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE +aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQx +MDAzMDAwMDAwWhcNMjUxMDE0MjM1OTU5WjBVMQswCQYDVQQGEwJFRTEQMA4GA1UE +BxMHVGFsbGlubjEbMBkGA1UEChMSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQQD +Ew5zaWQuZGVtby5zay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKAyy0yvjRCrATznThIwCu/wPCU5mV5UZIzNWl9KXx+gQiBp92SXfTOokkfiikBH +09HI+yVr3zI2U6FR8Tj21GiFE3bttmpCw8tJLmTe/P0Xah1D6vVkymbBt69N24ur +RqhW9in84WdkPc30vGJ+TdIj3jIePAbK3hHbpm+BfeyUhM48xXRgW+cBA//6R1C9 +lUaF9Ycylf+g/P7FpmzHRk2HF3bPyWziBVOhIADtqMyVEJk20dl0SWGsCmAJuAhM +mOPc87zpXYzlAlY24XgsTyQdDnqmJn8ZukDahIt9ybKH/WPLkZfw6xBnsQKXdG0J +HBqBsgQdPDFsrsY45o4ek0kCAwEAAaOCA4swggOHMB8GA1UdIwQYMBaAFHSFgMBm +x9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBSK7cmy40mto6zFVpcvnOyggb6YnzAZ +BgNVHREEEjAQgg5zaWQuZGVtby5zay5lZTA+BgNVHSAENzA1MDMGBmeBDAECAjAp +MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0P +AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0f +BIGXMIGUMEigRqBEhkJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRH +bG9iYWxHMlRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9j +cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAy +MENBMS0xLmNybDCBhwYIKwYBBQUHAQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC5kaWdpY2VydC5jb20wUQYIKwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRp +Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0x +LmNydDAMBgNVHRMBAf8EAjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdwAS +8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZJR+i+zAAAEAwBIMEYC +IQC7tPwb72Mur1ljtCP8g1/BkS6nJV0QeueW3eSa2L+PkwIhAPCJOyx++Vg5mE5D +6S0ctqbVRQsM5XGKYrBzAyzh0QHaAHYAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65 +Ay/ZDowuebgAAAGSUfovdQAABAMARzBFAiEA6ifcmc/Si0vOqT4JTAMqervuE7Uz +iYGZIIZI09BYINICICeJuQZrqP7aHqn9+0iyvl5ptJl2cZ5YyqF3Km9f6vu4AHYA +5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6hzId/R43jlAAAAGSUfovjAAABAMARzBF +AiEAkdK3dAY6ABFtaE1bTjIlYAF5cFT8N2pvxL0mA79LlDwCIFGZJ3EYJfxVbj9m +S/8FynieG/02iMF6xzmmrU58La0pMA0GCSqGSIb3DQEBCwUAA4IBAQCnq3OnD4uw +uvt75qYIBgFNN+nIMslacl8iQYSOswr+K90QzL/yf+lLafDX0QMtDL5b2t1a834R +8efjlEuISfp+YjTdtnNV1jZ7nnkHcFMP1MGbv/JQigPO8AgL+oxGHiRCp6FNJTwt +FtvHkqd5rDJUU988LdND4aYtmKYmGKj06sSqhpl9xmbIxdXPvaJGoHC/gEpM8AKw +oL4afke2q3FpjQ1eDT+37pjsEjQi6nT0/cSNoyxy4QbqWBgGclmb9ZAfOFkaO5U3 +bhRopdPzRSrQROUF0ovPk4aC+b74KAV/oxtQjPTdpdxTVBwjfn2tpes5q+TZUGSZ +AyP23gCAvmuj +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt index b16257c4..054743a0 100644 --- a/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt +++ b/src/test/resources/test-certs/TEST_SK_ROOT_G1_2021E.pem.crt @@ -1,17 +1,17 @@ ------BEGIN CERTIFICATE----- -MIICxDCCAiagAwIBAgIQGjWemJjC5ORg6CkyNQ5DzTAKBggqhkjOPQQDBDBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwHhcNMjEwNzA5MTA0NzE0WhcNNDEwNzA5MTA0NzE0WjBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABACGx6ye24WAORL1 -8N0SquoI3TTJ3dd2EcZLs+wZY0XWYzPa0S4o8BKZQTCDbXz9O2x94hpdAjZ4S3Q2 -N7DAvQ0FfAHmM2JotR4UnYvxYv4JxJHpoRvrQoXOXdqO/wMymiPKTXHPFQz6nxxa -ORjy8xsrQeIdrTLj3c+HDVBRA5yE/IXed6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFOIc3mPcvviEfgE7LkuAseF/1fHmMB8G -A1UdIwQYMBaAFOIc3mPcvviEfgE7LkuAseF/1fHmMAoGCCqGSM49BAMEA4GLADCB -hwJBNDZ3R6qmJqL5bQf01oT369DEGcLhr2vA00nRZSqeaaLMfq+RQW8aYl0njfIZ -JAC6q6IJklpH5IyYrcZ29tcBrxECQgFH5aw8ZORororrLDPl1yY2RgsCO1SFoDh5 -eMEaKVtRKNSG1jLzfgiZJOdtIj/h/l/4oDc5DrDDY6kbAnl4M5pDKw== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICxDCCAiagAwIBAgIQGjWemJjC5ORg6CkyNQ5DzTAKBggqhkjOPQQDBDBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzA5MTA0NzE0WhcNNDEwNzA5MTA0NzE0WjBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABACGx6ye24WAORL1 +8N0SquoI3TTJ3dd2EcZLs+wZY0XWYzPa0S4o8BKZQTCDbXz9O2x94hpdAjZ4S3Q2 +N7DAvQ0FfAHmM2JotR4UnYvxYv4JxJHpoRvrQoXOXdqO/wMymiPKTXHPFQz6nxxa +ORjy8xsrQeIdrTLj3c+HDVBRA5yE/IXed6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFOIc3mPcvviEfgE7LkuAseF/1fHmMB8G +A1UdIwQYMBaAFOIc3mPcvviEfgE7LkuAseF/1fHmMAoGCCqGSM49BAMEA4GLADCB +hwJBNDZ3R6qmJqL5bQf01oT369DEGcLhr2vA00nRZSqeaaLMfq+RQW8aYl0njfIZ +JAC6q6IJklpH5IyYrcZ29tcBrxECQgFH5aw8ZORororrLDPl1yY2RgsCO1SFoDh5 +eMEaKVtRKNSG1jLzfgiZJOdtIj/h/l/4oDc5DrDDY6kbAnl4M5pDKw== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer index 3cf570f6..b9da8a0c 100644 --- a/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer +++ b/src/test/resources/test-certs/TEST_of_SK_OCSP_RESPONDER_2020.pem.cer @@ -1,28 +1,28 @@ ------BEGIN CERTIFICATE----- -MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 -IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN -MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp -Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg -b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr -LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik -gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd -r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs -z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9 -OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb -wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg -RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE -FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D -AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A -aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w -JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME -GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw -czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN -BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR -aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo -MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA -nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24 -mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0 -dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9 +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290 +IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN +MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp +Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg +b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr +LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik +gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd +r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs +z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9 +OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb +wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg +RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE +FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D +AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A +aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w +JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME +GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw +czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN +BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR +aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo +MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA +nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24 +mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0 +dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt index cdb18ae6..74b2a841 100644 --- a/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt +++ b/src/test/resources/test-certs/auth-cert-40504040001-demo-q.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGqDCCBi6gAwIBAgIQDG5nZZQTInaS9mOLZwUmLDAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjUwOTA4MTIyNTIyWhcNMjgwOTA3MTIyNTIxWjBX -MQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzENMAsGA1UEBAwEVEVTVDEL -MAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkq -hkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjZXgGleu94rffU4c2iMM6J0eunUhbISt -ps5unECMwjKRohRJkwJlWToHv99Qs90vFM6rxMmZVKS6MZJ3WjkgbE0dxVVsWLnA -oBZWaZIidAgmQ6l0Cj0bLKjrRzKU/8T4gqy8tVrOJCvvwAlUIawzUdkQ+WdBCu79 -ipm2Lhh0KYkc20a9jNPfFwCOTkuO1AcVkhN6kdjIZgLrtlngKuhqNyotQmJ4Tl7+ -HkAtKV1zojIAY/LusoB5WWivdp6ey7hW4CyFAUhWpRnILcGeQ7sRBswQthEERThA -d5GRuiRWiz6eGrMAo7niINwoDNWwJDCeeAiJtYsjUBVyMyjxbPhX6DtwwZ3gP3NO -iwBQJ3qGDPNu0zGVdbph8+x9NZvkzAcPi8kr67S377BiH8AZPThzzkbadZxgIJrF -9WsTneKzAAk37HfGs3ESpFYaj8jlrI9RGJldh/nhTEdanqLhbGPO0gLXPJXR7pDG -+X9p6lC+sAa6mLVvBE5YSjQJoKCq8fsOfEQ0kyLcNem7rkxLh1ybsk57aDmZPPmx -Su853re+WvqJR8zPgD1qs7/LSKWeOOyi56OcVQ7Dmp6c56JH5JKQdUGbEXtkH+b6 -hHSMTXB/q/1OBwM4Tf7qO+AYU6MDVDdiEC0yAJm85b6cjbaxmY8a8eR2E3QuhV0H -7XLig9ebhoVb8cPJQmJxLMB1UHM4klQcav4F7+aaIOmEx60L0c7IITHU524LVjsP -GyqstXZxugSYh9V0h+aMdhtAj/JhWIh50fYrcaiyXZBu/nyRvzkY/6m5w/ycdiJ4 -tTNn+R5sJzdpo6SHQ5FnKRnpRnS7vrVSOy/quuYvvE06dE4fZrJfxAsGvg1L+qNT -yhpXECq9AF8U4Mqp03fnI5Dxglhx5rmNJZQKFM7PEDul0dV3lyAf6fmDPj/5H4L7 -RQyh5RcPEKbyk3Aw6v6RT69VuxXO2MvWDQx+S96Hk4105+BzWMWbOYyCWDRJz7IP -WTMQW5q1VnZTKElR1kxj6Ae7pITrlDDnAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAA -MB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQw -YjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k -ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIw -MjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1E -RU1PLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0 -cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlv -bi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsG -AQUFBwkBMREYDzE5OTkwMTAxMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUH -ADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIw -MjRlLmNybDAdBgNVHQ4EFgQUwT4r2UCtHWBnpnI3SjeaAGQyGlkwDgYDVR0PAQH/ -BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMGDy0L8r0KQxdmz7+6ZpzAtnoB7t6wP5 -YURwjJu5ysBMuYMIbw/5+R1Xv4nPo7BdQgIxAO3QXB8kg9w1jt8br7Sw2NUyUQZi -+Gt7a5Km+pxsMkiqCQYndI1Jrg/pW1gfUBRT2Q== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGqDCCBi6gAwIBAgIQDG5nZZQTInaS9mOLZwUmLDAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjUwOTA4MTIyNTIyWhcNMjgwOTA3MTIyNTIxWjBX +MQswCQYDVQQGEwJFRTEQMA4GA1UEAwwHVEVTVCxPSzENMAsGA1UEBAwEVEVTVDEL +MAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQwMDAxMIIDIjANBgkq +hkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjZXgGleu94rffU4c2iMM6J0eunUhbISt +ps5unECMwjKRohRJkwJlWToHv99Qs90vFM6rxMmZVKS6MZJ3WjkgbE0dxVVsWLnA +oBZWaZIidAgmQ6l0Cj0bLKjrRzKU/8T4gqy8tVrOJCvvwAlUIawzUdkQ+WdBCu79 +ipm2Lhh0KYkc20a9jNPfFwCOTkuO1AcVkhN6kdjIZgLrtlngKuhqNyotQmJ4Tl7+ +HkAtKV1zojIAY/LusoB5WWivdp6ey7hW4CyFAUhWpRnILcGeQ7sRBswQthEERThA +d5GRuiRWiz6eGrMAo7niINwoDNWwJDCeeAiJtYsjUBVyMyjxbPhX6DtwwZ3gP3NO +iwBQJ3qGDPNu0zGVdbph8+x9NZvkzAcPi8kr67S377BiH8AZPThzzkbadZxgIJrF +9WsTneKzAAk37HfGs3ESpFYaj8jlrI9RGJldh/nhTEdanqLhbGPO0gLXPJXR7pDG ++X9p6lC+sAa6mLVvBE5YSjQJoKCq8fsOfEQ0kyLcNem7rkxLh1ybsk57aDmZPPmx +Su853re+WvqJR8zPgD1qs7/LSKWeOOyi56OcVQ7Dmp6c56JH5JKQdUGbEXtkH+b6 +hHSMTXB/q/1OBwM4Tf7qO+AYU6MDVDdiEC0yAJm85b6cjbaxmY8a8eR2E3QuhV0H +7XLig9ebhoVb8cPJQmJxLMB1UHM4klQcav4F7+aaIOmEx60L0c7IITHU524LVjsP +GyqstXZxugSYh9V0h+aMdhtAj/JhWIh50fYrcaiyXZBu/nyRvzkY/6m5w/ycdiJ4 +tTNn+R5sJzdpo6SHQ5FnKRnpRnS7vrVSOy/quuYvvE06dE4fZrJfxAsGvg1L+qNT +yhpXECq9AF8U4Mqp03fnI5Dxglhx5rmNJZQKFM7PEDul0dV3lyAf6fmDPj/5H4L7 +RQyh5RcPEKbyk3Aw6v6RT69VuxXO2MvWDQx+S96Hk4105+BzWMWbOYyCWDRJz7IP +WTMQW5q1VnZTKElR1kxj6Ae7pITrlDDnAgMBAAGjggH1MIIB8TAJBgNVHRMEAjAA +MB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAGCCsGAQUFBwEBBGQw +YjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9FSUQtUV8yMDI0RS5k +ZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkcTIw +MjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00MDUwNDA0MDAwMS1E +RU1PLVEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQIwVjBUBggrBgEFBQcCARZIaHR0 +cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMvY2VydGlmaWNhdGlv +bi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBAjAoBgNVHQkEITAfMB0GCCsG +AQUFBwkBMREYDzE5OTkwMTAxMTIwMDAwWjAWBgNVHSUEDzANBgsrBgEEAYPmYgUH +ADA0BgNVHR8ELTArMCmgJ6AlhiNodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1xXzIw +MjRlLmNybDAdBgNVHQ4EFgQUwT4r2UCtHWBnpnI3SjeaAGQyGlkwDgYDVR0PAQH/ +BAQDAgeAMAoGCCqGSM49BAMDA2gAMGUCMGDy0L8r0KQxdmz7+6ZpzAtnoB7t6wP5 +YURwjJu5ysBMuYMIbw/5+R1Xv4nPo7BdQgIxAO3QXB8kg9w1jt8br7Sw2NUyUQZi ++Gt7a5Km+pxsMkiqCQYndI1Jrg/pW1gfUBRT2Q== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-cert-40504040001.pem.crt b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt index 643909f7..cfaeac6b 100644 --- a/src/test/resources/test-certs/auth-cert-40504040001.pem.crt +++ b/src/test/resources/test-certs/auth-cert-40504040001.pem.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQy -bEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7C -XahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEO -TnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5n -zCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcy -hPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY -0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29Jc -owTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943 -iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9Ps -mN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1Jg -xBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr -/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRj -Fisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QA -CBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq -8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v0 -9hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSg -At+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHx -MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYI -KwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJ -RC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5z -ay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQw -NTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsG -AQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9j -ZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1Ud -CQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0G -CysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rl -c3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHK -mTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0X -nPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxW -Cbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2 ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGszCCBjmgAwIBAgIQZDoy+8wlWu/meKNnbvNU4zAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDITANBgkqhkiG9w0BAQEFAAOCAw4AMIIDCQKCAwBl1gMBN2d0nnUslMQy +bEt5L1lGBpHymjm99i8TrtjeijrPy+XLZZqWb1sqc33rI8t6GK/MfKu1W0ulAW7C +XahUTpxMHNDJus82jqyESu5M9fMpyEmLQkRccINWopRF8BCoCrvhQ8eiYCMlwlEO +TnGp8daxjmJ746jp0ZVGc+HqVX3ON505Mryu14fxfuaiT3lR48impyKEgThk8G5n +zCABn41j/lBx7BasePNoL27FuPxtHpbZtislWNqk8WkFjbAoh7vyz1QGuxHYSGcy +hPtPogbNpGsZPYVYd0WqqmeWAEcXCfd2vjjNFajHO4HVcJZd3rYKPVxMsd/+NuAY +0b8gG3S/+dlxjjjHcQtMP5PPy6363wwsPt50pfdJpT12KtooeDlEH5ja3hGx29Jc +owTdpCKENxzGQu/UY4gN4dPnqJtkWC5mPFwDCutONge5PPU6IYKzx5Hom/c2S943 +iGR5QXAyGxNqtgfFTsDF2VWexv+N857z8Mt8iHC8cfYvuOzLGgUQ7huSHJaEG9Ps +mN2DRMkiIGehgzWTkb7KF/zKBy5WenrQJvsYMnR9WClUPYz8pO3aYxTU0BGIF1Jg +xBweIyU0rnQKeELC7bl1JQ0Ir2xBFT+RzQqQClgcdDjjRnotz4H2G8SX1oCmGbcr +/c8OD8yVBBo88An20V64EQTJ7yFUS+O5XLla6oBIDSSn/kE4oKudHSB6NicoObRj +Fisr+6E84j0N2BxGcFa+To6aIWAxx1l8VU5wLwv3gKoZblOeGUQExE0/4FDv19QA +CBAT9J7ShoREqXJjRk2HaajGa3TBYa5Veh7LXqYS4S7DrvzMuJ6BMwsHchroGrUq +8Hh4utAlbcCdMgGLWhM10M1daxlIqwBSg3JINfK+nvhx13QsLHfMk9upueCxF2v0 +9hdtubp1gyfQE1mXRAbJsXvQ39bG7gsascnsypOhTCktx91Cj0k2W23/oIVMitSg +At+G05LbLB8RtomESnxTTqlAOyGW2ahMd7OYsADeSLVkRtMCAwEAAaOCAfUwggHx +MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsCQXGYjjZvjNKFhle00U2JJmT2swcAYI +KwYBBQUHAQEEZDBiMDMGCCsGAQUFBzAChidodHRwOi8vYy5zay5lZS9URVNUX0VJ +RC1RXzIwMjRFLmRlci5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9haWEuZGVtby5z +ay5lZS9laWRxMjAyNGUwMAYDVR0RBCkwJ6QlMCMxITAfBgNVBAMMGFBOT0VFLTQw +NTA0MDQwMDAxLU1PQ0stUTB4BgNVHSAEcTBvMGMGCSsGAQQBzh8RAjBWMFQGCCsG +AQUFBwIBFkhodHRwczovL3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9j +ZXJ0aWZpY2F0aW9uLXByYWN0aWNlLXN0YXRlbWVudC8wCAYGBACPegECMCgGA1Ud +CQQhMB8wHQYIKwYBBQUHCQExERgPMTkwNTA0MDQxMjAwMDBaMBYGA1UdJQQPMA0G +CysGAQQBg+ZiBQcAMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jLnNrLmVlL3Rl +c3RfZWlkLXFfMjAyNGUuY3JsMB0GA1UdDgQWBBQPJVXjvOMJVVa3SLDo7GmgSIHK +mTAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwMDaAAwZQIxANE5eGxxEuTk/d0X +nPqi//hWU5CbC+IhdqUi+18HeFVZ7pKh1/canmUCGqdfpkt/uwIwAXNL/3130MxW +Cbs82tV7wWEEI4iA7jI5wcQgUDD88BDyI5wxrgTSEso9iuH7k0a2 +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt index 9c3a45b6..7df0ee18 100644 --- a/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt +++ b/src/test/resources/test-certs/auth-pnolv-020100-29990-mock-q.crt @@ -1,46 +1,46 @@ ------BEGIN CERTIFICATE----- -MIIIDTCCBfWgAwIBAgIQM34W+9hlpYRjtEHrK9yczDANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwIBcNMjMwMTAzMTQ1NTM5WhgPMjAzMDEyMTcyMzU5NTlaMGoxCzAJ -BgNVBAYTAkxWMRkwFwYDVQQDDBBURVNUTlVNQkVSLEFEVUxUMRMwEQYDVQQEDApU -RVNUTlVNQkVSMQ4wDAYDVQQqDAVBRFVMVDEbMBkGA1UEBRMSUE5PTFYtMDIwMTAw -LTI5OTkwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAp4yVDWinZexD -b/OUB7U414CuMbmzgo7ApXVML2sTU55XBbcQUCo15wkO9gP6u72YZ1TiEN1wbFxw -mBGGoVm5RRi/1WdQhSRcZ3TOPwE06VILayZ6kQo+D6WbqwazgWuCPYl858xe0E9k -PkEKeNhX3LgDVUFW9kq09HVPqJ2e4anw+eAF78bxjp2pN49IziZBsUjNjmcmartp -Qf5nFJEqIC3m+67oQoqkRtfTX7eKF5+pjoq6XzFgkwdp4Xnfq3wgN+fmqJF8tGeL -1jQxMqWWuqhwMUaklbW4s+M3vGVMQd5rDnBl5qRCPn+gNxseKAaueUchI2WGjOKU -QM7MSay0qeYd3s435cRwGub7asY6p/7Nn19ykQDPj9bV35eZ1GLL7Sb/QYXBUTcP -fd1lxjJwNYJIbpJ0Aj/qtgmGGbWa2ROs9v2l2jh8Re2YRaIvYVrVTiCHFOuMf+PH -O36qFOWdbGtZnYT3QEPQVe0DCXhsioMndEo/BSASGmgriqBaS+T6x9UcS6qbRkQ5 -m0piwaDrwaBSyP4oW60jbLA9SvWDancGOLXqOgEIyhdxuB3w0TFLytjZMq0olLjy -XeK6XL1A052CBu97/8GmZkTelg9qYMJmrVUaidby16bYUgjxSV9O+0w+ZzVDUuwE -p5LUQwoEBrylA2fD8gk9JhffsbbmrgvEOCk8f590KRt4JczND+WmmaXGTNv9Rbj2 -9pv0wqlpMV8iaPxxvdNydlE51K9baxopZibZdH3abiRhsyffDZckZHNIzExzE/V9 -6MYh9Myos8dDSR+HPg3F1i92PjsusJUMUyZFcs0cAWXV0Z/4G7yB4J1YeJx6GkbD -PLG8G1kvnh1mJAxmm/39MK3gHINIe1+JNc8c32Y75JDF4yjQftP0lwHx/WHwfvkq -nmLHYc9Sb2o/AYjbTsxOxqhnmkS/smn4R/t3/i4gzfvGnhc9Mh/5OduGeTWAP4CE -M6pgsCRlcGNwRNTzw3yW4nNZdFIsINru6Gtwy3PlQzuNjT9lvgt9AgMBAAGjggGt -MIIBqTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBcBgNVHSAEVTBTMEcGCisG -AQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1 -L2VuL3JlcG9zaXRvcnkvQ1BTLzAIBgYEAI96AQIwHQYDVR0OBBYEFBEOFq9GzHMr -rV6qgBoEw1Y9lEquMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MBMG -A1UdJQQMMAoGCCsGAQUFBwMCMHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYd -aHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6 -Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0 -MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMjAxMDAtMjk5OTAtTU9D -Sy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwMDAxMDIxMjAwMDBaMA0G -CSqGSIb3DQEBCwUAA4ICAQCwZnf+COHoIlkxIFPU33TCmh//k5oVDp42pK7Zp765 -KIppDRuxVtFHNvM5F4P9lGQ1FycPi+8N6uDX+XboKQ5SwtvcKYL23GfQwxnzMc0h -lyFm9Gx5Etl200CIP0hTCiFpEWouIOy3spGXwoaFjrcL+oYuL0HW7kFORSjerwOL -osTHJsT1geDY64INgO98i7WgHnmtMjoVXeyklVCsKwvYnMZVFzrpQkL7h9CQGffq -4S664jGYEghnZXh8uiq8x3l4V777NPuwCspiebXUrEmUe9lP/dHwaX009y4gygkB -VQSr7z8QTh3Cbt2g9Brt+w/PqKmYw+eJyQ21DbxPrQyZKQvFY+XsWkyWOFrRPsG+ -Rb7lqvejm8ppqQX7wH6ulvhkKT91vF1uy2icb77VB5i3m7LMSZF7BUa/U6Qlm2GK -Cz8+6FN3xiC+cMulWaMA7c0tiT9aWTqqPh5w9RfAIWXgbsG0vP+vSMtyRERcMpA+ -hjzB6Rj44j0Mg4PfKpvlsYG2aF5NXNRJpT2utm+Var44+HthQkltoWhGjXG9Rc7c -MQBRECohUqeNtV0GCQUnE2RtZvufidVw4sOx3qxmoU/dlribucQ4mVPZjVF3LVoX -IYJRUqtdMrh9dR9ZDAwuA1Y6sxr3Dw/QQujtU8NKAAGKIPsPVAir5Dfs7R+bF/dR -mg== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIIDTCCBfWgAwIBAgIQM34W+9hlpYRjtEHrK9yczDANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMjMwMTAzMTQ1NTM5WhgPMjAzMDEyMTcyMzU5NTlaMGoxCzAJ +BgNVBAYTAkxWMRkwFwYDVQQDDBBURVNUTlVNQkVSLEFEVUxUMRMwEQYDVQQEDApU +RVNUTlVNQkVSMQ4wDAYDVQQqDAVBRFVMVDEbMBkGA1UEBRMSUE5PTFYtMDIwMTAw +LTI5OTkwMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAp4yVDWinZexD +b/OUB7U414CuMbmzgo7ApXVML2sTU55XBbcQUCo15wkO9gP6u72YZ1TiEN1wbFxw +mBGGoVm5RRi/1WdQhSRcZ3TOPwE06VILayZ6kQo+D6WbqwazgWuCPYl858xe0E9k +PkEKeNhX3LgDVUFW9kq09HVPqJ2e4anw+eAF78bxjp2pN49IziZBsUjNjmcmartp +Qf5nFJEqIC3m+67oQoqkRtfTX7eKF5+pjoq6XzFgkwdp4Xnfq3wgN+fmqJF8tGeL +1jQxMqWWuqhwMUaklbW4s+M3vGVMQd5rDnBl5qRCPn+gNxseKAaueUchI2WGjOKU +QM7MSay0qeYd3s435cRwGub7asY6p/7Nn19ykQDPj9bV35eZ1GLL7Sb/QYXBUTcP +fd1lxjJwNYJIbpJ0Aj/qtgmGGbWa2ROs9v2l2jh8Re2YRaIvYVrVTiCHFOuMf+PH +O36qFOWdbGtZnYT3QEPQVe0DCXhsioMndEo/BSASGmgriqBaS+T6x9UcS6qbRkQ5 +m0piwaDrwaBSyP4oW60jbLA9SvWDancGOLXqOgEIyhdxuB3w0TFLytjZMq0olLjy +XeK6XL1A052CBu97/8GmZkTelg9qYMJmrVUaidby16bYUgjxSV9O+0w+ZzVDUuwE +p5LUQwoEBrylA2fD8gk9JhffsbbmrgvEOCk8f590KRt4JczND+WmmaXGTNv9Rbj2 +9pv0wqlpMV8iaPxxvdNydlE51K9baxopZibZdH3abiRhsyffDZckZHNIzExzE/V9 +6MYh9Myos8dDSR+HPg3F1i92PjsusJUMUyZFcs0cAWXV0Z/4G7yB4J1YeJx6GkbD +PLG8G1kvnh1mJAxmm/39MK3gHINIe1+JNc8c32Y75JDF4yjQftP0lwHx/WHwfvkq +nmLHYc9Sb2o/AYjbTsxOxqhnmkS/smn4R/t3/i4gzfvGnhc9Mh/5OduGeTWAP4CE +M6pgsCRlcGNwRNTzw3yW4nNZdFIsINru6Gtwy3PlQzuNjT9lvgt9AgMBAAGjggGt +MIIBqTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIEsDBcBgNVHSAEVTBTMEcGCisG +AQQBzh8DEQIwOTA3BggrBgEFBQcCARYraHR0cHM6Ly9za2lkc29sdXRpb25zLmV1 +L2VuL3JlcG9zaXRvcnkvQ1BTLzAIBgYEAI96AQIwHQYDVR0OBBYEFBEOFq9GzHMr +rV6qgBoEw1Y9lEquMB8GA1UdIwQYMBaAFK6w6uE2+CarpcwLZlX+Oh0CvxK0MBMG +A1UdJQQMMAoGCCsGAQUFBwMCMHwGCCsGAQUFBwEBBHAwbjApBggrBgEFBQcwAYYd +aHR0cDovL2FpYS5kZW1vLnNrLmVlL2VpZDIwMTYwQQYIKwYBBQUHMAKGNWh0dHA6 +Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tfMjAxNi5kZXIuY3J0 +MDEGA1UdEQQqMCikJjAkMSIwIAYDVQQDDBlQTk9MVi0wMjAxMDAtMjk5OTAtTU9D +Sy1RMCgGA1UdCQQhMB8wHQYIKwYBBQUHCQExERgPMjAwMDAxMDIxMjAwMDBaMA0G +CSqGSIb3DQEBCwUAA4ICAQCwZnf+COHoIlkxIFPU33TCmh//k5oVDp42pK7Zp765 +KIppDRuxVtFHNvM5F4P9lGQ1FycPi+8N6uDX+XboKQ5SwtvcKYL23GfQwxnzMc0h +lyFm9Gx5Etl200CIP0hTCiFpEWouIOy3spGXwoaFjrcL+oYuL0HW7kFORSjerwOL +osTHJsT1geDY64INgO98i7WgHnmtMjoVXeyklVCsKwvYnMZVFzrpQkL7h9CQGffq +4S664jGYEghnZXh8uiq8x3l4V777NPuwCspiebXUrEmUe9lP/dHwaX009y4gygkB +VQSr7z8QTh3Cbt2g9Brt+w/PqKmYw+eJyQ21DbxPrQyZKQvFY+XsWkyWOFrRPsG+ +Rb7lqvejm8ppqQX7wH6ulvhkKT91vF1uy2icb77VB5i3m7LMSZF7BUa/U6Qlm2GK +Cz8+6FN3xiC+cMulWaMA7c0tiT9aWTqqPh5w9RfAIWXgbsG0vP+vSMtyRERcMpA+ +hjzB6Rj44j0Mg4PfKpvlsYG2aF5NXNRJpT2utm+Var44+HthQkltoWhGjXG9Rc7c +MQBRECohUqeNtV0GCQUnE2RtZvufidVw4sOx3qxmoU/dlribucQ4mVPZjVF3LVoX +IYJRUqtdMrh9dR9ZDAwuA1Y6sxr3Dw/QQujtU8NKAAGKIPsPVAir5Dfs7R+bF/dR +mg== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/ca-cert.pem.crt b/src/test/resources/test-certs/ca-cert.pem.crt index b6d7af73..5ca038c0 100644 --- a/src/test/resources/test-certs/ca-cert.pem.crt +++ b/src/test/resources/test-certs/ca-cert.pem.crt @@ -1,23 +1,23 @@ ------BEGIN CERTIFICATE----- -MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw -bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s -dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow -cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx -FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv -bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 -oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh -vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC -AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L -gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr -LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo -dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw -VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv -dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy -MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j -cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB -BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f -4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo -XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa -7Wy8pf2lw6EcfyU= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert index b49f0d45..e27cb966 100644 --- a/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert +++ b/src/test/resources/test-certs/cert-choice-cert-40504040001.pem.cert @@ -1,42 +1,42 @@ ------BEGIN CERTIFICATE----- -MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ -q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw -SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL -r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma -tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB -0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W -rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY -1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp -RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH -48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 -nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk -aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e -wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk -pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM -ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f -jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY -xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC -izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG -CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u -c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 -MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD -VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF -BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA -jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov -L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 -c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov -L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU -1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 -7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC -MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld -CQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/expired-cert.pem.crt b/src/test/resources/test-certs/expired-cert.pem.crt index 03d919f4..8e18defd 100644 --- a/src/test/resources/test-certs/expired-cert.pem.crt +++ b/src/test/resources/test-certs/expired-cert.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkG -A1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYD -VQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ -Tk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0G -CSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51ISh -Sj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+Ml -lSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5cz -GP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdc -DmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncv -pRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhN -BYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04 -nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a -4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2 -wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSno -JPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoA -xoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIE -sDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6 -Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4E -FgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtm -Vf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkG -CCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEF -BQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tf -MjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdF -TOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDi -G7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbr -rl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxM -TEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl -4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1 -ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y5 -2n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf -83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAK -Gn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbR -R1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2P -MnXRUgtlnV7ykFpmkR4JcQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGzDCCBLSgAwIBAgIQfj3go7LifaBZQ5AvISB2wjANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwHhcNMTcwNjE2MDgwMDQ3WhcNMjAwNjE2MDgwMDQ3WjCBjjELMAkG +A1UEBhMCRUUxETAPBgNVBAQMCFNNQVJULUlEMQ0wCwYDVQQqDARERU1PMRowGAYD +VQQFExFQTk9FRS0xMDEwMTAxMDAwNTEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEXMBUGA1UECwwOQVVUSEVOVElDQVRJT04wggIhMA0G +CSqGSIb3DQEBAQUAA4ICDgAwggIJAoICAFmtxMhB0U+EjR0UM1uVdxcjA7l51ISh +Sj4wvZVh7HAdXLMg7JzfsMy3Ei5nYVG/Pen8wMSFE2qzbkD/JLsxdzEapYFyc+Ml +lSi3BR/3d8PYLO+LR5nURX/8c90EHjO3L5LcYp6qmT9sm1uVR9ypp4vkNucOs5cz +GP0NAO9hEtO08Hz2OL/p9Z/9sKYg2YREWw9WP9KbAlnPc7mbNPkdgbnmXr9BPJdc +DmYuBxUXHntvRpiKKQDwnG0ar2XHwutoQKNsbxgoqOVwtetAewfgITLruYxaXncv +pRSnHqn7pebVAlMqK6vmuW4+mJUCgu64Qjv4GPbdm5d3uM4KXUrKaV3hyRn9FGhN +BYgtDGFvnL2soapXngvE9bRka4ZxrB3Fv6F2eFk37Kb6lM4RMC4q3LIbxNJdMC04 +nXoQbmDK5oqY6mUON+ITcs76nIv+8atx936lPWX/JZXpR4TaY9AwLEkWdA/tp0+a +4pfGoktSyXjK2gGjuEOrzo4Z+1xCrQnLcViD9ZZr9lcJE2URBPI1SYMSjN9/c7e2 +wCziLY+RLifTcFFMiBAYtYgubgfQffJCuIrL8Tit9uRPM7pxM3v+Pm1YJFKSsSno +JPAxZAkVXFhdJjX0NKHcdOSCTTCaCvIbWTGVSIiIBArRQm0BxqcuejiXOpd1ysoA +xoe7RsMhJfxHAgMBAAGjggFKMIIBRjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIE +sDBVBgNVHSAETjBMMEAGCisGAQQBzh8DEQIwMjAwBggrBgEFBQcCARYkaHR0cHM6 +Ly93d3cuc2suZWUvZW4vcmVwb3NpdG9yeS9DUFMvMAgGBgQAj3oBATAdBgNVHQ4E +FgQU0sO+sbSuwBDd7fEpaUtr3NRiSXkwHwYDVR0jBBgwFoAUrrDq4Tb4JqulzAtm +Vf46HQK/ErQwEwYDVR0lBAwwCgYIKwYBBQUHAwIwfQYIKwYBBQUHAQEEcTBvMCkG +CCsGAQUFBzABhh1odHRwOi8vYWlhLmRlbW8uc2suZWUvZWlkMjAxNjBCBggrBgEF +BQcwAoY2aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVTVF9vZl9FSUQtU0tf +MjAxNi5kZXIuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQALw3Jv/4PMSsihmSLE7kdF +TOaaBsT+VVPT9MG2+vcmNeMafK+54xrkgjTWdrG8AevfQK++2zOa4QZ3O7/xKpDi +G7bxs9jSsEV482IA6GyzdyMj+FSnLjJZO1rFYPIM5cZ6kici7bH3cbQxHkR5kIbr +rl/8Mx1uBpVPg7uFyqSPZb1/1w65aKxa25ZLsLQPlNscZl8/nZHoIz84fp2zduxM +TEt559m6OhyiVcYZLvn5Isaph7PO+46OawcSkDLHHyFCvsBqODO6LkvHM34ncgIl +4zae8G+CaY8samXOGu1mvnlPxQxHh5qFZHoBaMdYvGqUj24lAKQp5QZQuAGhV+a1 +ooYMbeelhdZZMHXbI/5sUIzWnnTOevpYQgwdztyFkSwuYNJ2NuZTD6zeHnTaw7Y5 +2n4DCudsi0eCjZ3GYmcZEVz5VAf4Cx0fSnImFgIP75R+aYD6dmJVkyar5rAGrfwf +83JB+7rgOd84R73+zDvo0MLpCLGteAIiDimT8H7Uu+HCfvpOWsKnVuVVcDJRzwAK +Gn451QGTHwL0iIRGC8Xs1m/8iU7IiZ6zuQ0Xpil4fSUO3txVbEDQomgsj0mTZRbR +R1gNtAPQCSdMhRtU78RyKGyRTpX5nawWaxi8aAjeSgUr+kd/He73RTneNEWYMy2P +MnXRUgtlnV7ykFpmkR4JcQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/nq-auth-cert-40504049999.crt b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt index 1f941975..dd4d987b 100644 --- a/src/test/resources/test-certs/nq-auth-cert-40504049999.crt +++ b/src/test/resources/test-certs/nq-auth-cert-40504049999.crt @@ -1,38 +1,38 @@ ------BEGIN CERTIFICATE----- -MIIGjjCCBhSgAwIBAgIQM1MMPZyR0fwWexCl4aAn6zAKBggqhkjOPQQDAzByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMB4XDTI0MTAxNTE4NDMxOFoXDTI3MTAxNTE4NDMxN1ow -YzELMAkGA1UEBhMCTFQxFjAUBgNVBAMMDVRFU1ROVU1CRVIsT0sxEzARBgNVBAQM -ClRFU1ROVU1CRVIxCzAJBgNVBCoMAk9LMRowGAYDVQQFExFQTk9MVC00MDUwNDA0 -OTk5OTCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAcLdOmpoXwhh41xRM -MqkTl7EVlD7kFgQxX32yVKDGG1wIB/iEaIlc/JRmPwpPVV996gbaBsOmsTSkdhyo -QuqLiov/gQ8a5/ByKBgGxLflCgtV6SsVqzJZoPBhs0KiVKwTWgA2aLv+oaKcDbrv -40Tz5J0DrgevAQctYm6eM1plJ1n/J5sINFgdXHv1k6iqSCMKb35vLZiywHoXo11H -ERX6HcfoYgaBcuWTTv5c6XS4nbfl2JzEsXpzshifRLwU/gQvkHqWiUv3TwpaRVrP -/ClzJP+s8fwx4mGgW0J4tSQqt4HWLpZOEu4ksIKijbQc7wLN006cV1O8EkZZhXud -bJWS3JJtmiFMwMc6fWa07UuMYHho86jZkKbJkhgQFX4TNzSm3O3CABNaVvqElagp -4OMYBbbe0HPPlEWGyoApvxVHaWa2mwtKomjZw11BuqZf7HevAKhURbOV8ImMzxii -woTfsBSyHniDW/mOy5hfgtkrvpXsy0BQ6NjdMcPLGW1Zp5DgrzjxXQhpJDzBUrna -eBQjKhh51a/siuby6eblm73gBHVLzDRK1Im0l4als5Lw/xoc8exlhFJhfg7+9L9a -NfUQAJWnUJ0cM3fd9O/PqqWlbk/R/1RUuIIhlmPhiPO+s/CWPZqbOIiasP1AyQMQ -N5p2+KRIQ9/RmoGz+fEzk063wR62YUms1iU7GTqqjSsficdVAfsnOQhsVwUXfOJx -ob2UYOh/RzEB9Uo+Mhwu4Omgxgl8PIL3nj/Lj8KmbdvUeAB9JgZ2xc5gbPY4GL4e -ShcyuxaNM4T/4N08WarBExI5GwJgqgaYA/J3agrqe9tcZJjlBPFT1A/O5Lc3Ewn8 -xlSvWe3r695PLf73ADi/vhjhmznqp72AmMYfmaj/WecS0FSfRPQP7ovb1N5DjIEx -U6GNF1Lb/9Q/z56uMf2y0tU35F4y/zhnBdZ+eSa69DoKD49RHRjMTA+UJNM4DZrN -uBszXI30OmlvGYZdzETQU52CpR79E2svr1hMto2G2/TA4YzzAgMBAAGjggHPMIIB -yzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIG -CCsGAQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtTlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1v -LnNrLmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0xU -LTQwNTA0MDQ5OTk5LU1PQ0stTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBU -BggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJj -ZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATAW -BgNVHSUEDzANBgsrBgEEAYPmYgUHADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8v -Yy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5jcmwwHQYDVR0OBBYEFBetuv93k5ps -ZBo46kmtdw4TGe24MA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAwNoADBlAjEA -llFf3tx3m9UVEhCmg3MFBtkD7rCaK6MSym/DEhR7LfXXOIWEuVAC0eaX8T2DyXxw -AjB1WKZRYM6awmjUpt3CdRSbQ0DcZbpWxjYjuHM3zkt2XkDwvXwEcfJbnnetIDDT -tSE= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGjjCCBhSgAwIBAgIQM1MMPZyR0fwWexCl4aAn6zAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI0MTAxNTE4NDMxOFoXDTI3MTAxNTE4NDMxN1ow +YzELMAkGA1UEBhMCTFQxFjAUBgNVBAMMDVRFU1ROVU1CRVIsT0sxEzARBgNVBAQM +ClRFU1ROVU1CRVIxCzAJBgNVBCoMAk9LMRowGAYDVQQFExFQTk9MVC00MDUwNDA0 +OTk5OTCCAyEwDQYJKoZIhvcNAQEBBQADggMOADCCAwkCggMAcLdOmpoXwhh41xRM +MqkTl7EVlD7kFgQxX32yVKDGG1wIB/iEaIlc/JRmPwpPVV996gbaBsOmsTSkdhyo +QuqLiov/gQ8a5/ByKBgGxLflCgtV6SsVqzJZoPBhs0KiVKwTWgA2aLv+oaKcDbrv +40Tz5J0DrgevAQctYm6eM1plJ1n/J5sINFgdXHv1k6iqSCMKb35vLZiywHoXo11H +ERX6HcfoYgaBcuWTTv5c6XS4nbfl2JzEsXpzshifRLwU/gQvkHqWiUv3TwpaRVrP +/ClzJP+s8fwx4mGgW0J4tSQqt4HWLpZOEu4ksIKijbQc7wLN006cV1O8EkZZhXud +bJWS3JJtmiFMwMc6fWa07UuMYHho86jZkKbJkhgQFX4TNzSm3O3CABNaVvqElagp +4OMYBbbe0HPPlEWGyoApvxVHaWa2mwtKomjZw11BuqZf7HevAKhURbOV8ImMzxii +woTfsBSyHniDW/mOy5hfgtkrvpXsy0BQ6NjdMcPLGW1Zp5DgrzjxXQhpJDzBUrna +eBQjKhh51a/siuby6eblm73gBHVLzDRK1Im0l4als5Lw/xoc8exlhFJhfg7+9L9a +NfUQAJWnUJ0cM3fd9O/PqqWlbk/R/1RUuIIhlmPhiPO+s/CWPZqbOIiasP1AyQMQ +N5p2+KRIQ9/RmoGz+fEzk063wR62YUms1iU7GTqqjSsficdVAfsnOQhsVwUXfOJx +ob2UYOh/RzEB9Uo+Mhwu4Omgxgl8PIL3nj/Lj8KmbdvUeAB9JgZ2xc5gbPY4GL4e +ShcyuxaNM4T/4N08WarBExI5GwJgqgaYA/J3agrqe9tcZJjlBPFT1A/O5Lc3Ewn8 +xlSvWe3r695PLf73ADi/vhjhmznqp72AmMYfmaj/WecS0FSfRPQP7ovb1N5DjIEx +U6GNF1Lb/9Q/z56uMf2y0tU35F4y/zhnBdZ+eSa69DoKD49RHRjMTA+UJNM4DZrN +uBszXI30OmlvGYZdzETQU52CpR79E2svr1hMto2G2/TA4YzzAgMBAAGjggHPMIIB +yzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIG +CCsGAQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtTlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1v +LnNrLmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0xU +LTQwNTA0MDQ5OTk5LU1PQ0stTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBU +BggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJj +ZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATAW +BgNVHSUEDzANBgsrBgEEAYPmYgUHADA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8v +Yy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5jcmwwHQYDVR0OBBYEFBetuv93k5ps +ZBo46kmtdw4TGe24MA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAwNoADBlAjEA +llFf3tx3m9UVEhCmg3MFBtkD7rCaK6MSym/DEhR7LfXXOIWEuVAC0eaX8T2DyXxw +AjB1WKZRYM6awmjUpt3CdRSbQ0DcZbpWxjYjuHM3zkt2XkDwvXwEcfJbnnetIDDT +tSE= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/nq-signing-cert.pem b/src/test/resources/test-certs/nq-signing-cert.pem index 9713a2e9..d1aedc43 100644 --- a/src/test/resources/test-certs/nq-signing-cert.pem +++ b/src/test/resources/test-certs/nq-signing-cert.pem @@ -1,37 +1,37 @@ ------BEGIN CERTIFICATE----- -MIIGdDCCBfmgAwIBAgIQayQDbT+81MKPikMhkxHiEjAKBggqhkjOPQQDAzByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMB4XDTI1MDgzMTA1NTkyNVoXDTI4MDgzMDA1NTkyNFow -XzELMAkGA1UEBhMCRkkxFDASBgNVBAMMC01VU0VSLFVSTUFTMQ4wDAYDVQQEDAVN -VVNFUjEOMAwGA1UEKgwFVVJNQVMxGjAYBgNVBAUTEVBOT0ZJLTM5MDAzMDEyNzk4 -MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAmX7GLKF5e8LxXrRNl5sM -AHA3UwM9/RKIjAePphFJ1IwEu4f1NutqGWs9hhsh6PnBnbnKsvN/US//5Bw/t37z -gBqgQaToPCww9zHsUFl0w3Q+XGxK3wzKltu50WaxIQaLExFTgFsPmViabT7M1Kqu -6UYlJQJQPkgkfvmuz3LeCFXFRpjEoIgvVfbAYxXfn8V1HPwPAhFTkC1iTht14SOE -WolghzV5R3IYeCey8Y978rFy24avN+ea1aweti98UaFH8wIjuX1zND7SHY2fu5tF -+uxSacJuBUukL0w34n3ODEPDJXojPEnJEIOZmJIV35jGcTMEc6OVlKHYBfwISUjy -+/dIy+oq/qz7z5Kr4gv2DBTLpEj6bCgS2uDDxVXZbDFY3K0jXtxVtk11pWKFUEtd -YJFV6FRswFHF/WYp8o2WJ7O0hlB47pkUgLtKXLNJrS8z+dLcP3s626ZSjfEPf3LD -Ku9GoDetBzvj+QanNUpzZGpWdaMNN4dpLdMSyp2WK6KK3TXowmYYSaXS2t+5MyM9 -0Om3JUkY9hIBIfeAgGwA9Vx8OT5srYU1zWIGcP8AeTsE2JDpCh11M56dAw8yF2z0 -JyTjYcmmX/Zla3gT76yhL43AHT64hxohpiZr8R2lKSFO1qg7ECly1XKov5Vm8rTd -CaDiWLrBwh4+XEJSludG6KZ+0Q8hrGU23qNBH/Yo4xh2vArwdaoKpHUiMbwu9/MZ -k7WTgCqxnp9fC6l+25ePfK4xiA5iVfZsmBs8iE8Z/J3akd2tZRW3jE672F1L+V6m -icbVCi8VFpWcVfdTZFvQPoozZAx8dm//L4hEEeu/7ylEmrn4luEcMsk386Pj61KB -wk54zzuxWc9i3u4JyW3B6F87LhGv01osVK35mXWhBI5hN8YrRHieHafuMGrfdGp9 -XrKRleYtih2BYr+FD2bfaxf0KEBaQdHMXEZ814x3SYLb1iv7/5yz6FNB37OwlIwa -VvoxaIggGXI0Ht0axQ6dqvDdo1cH+uXNYZJtp+ScQ6qVAgMBAAGjggG3MIIBszAJ -BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIGCCsG -AQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9FSUQt -TlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1vLnNr -LmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0ZJLTM5 -MDAzMDEyNzk4LTlUMTctTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATA1BgNV -HR8ELjAsMCqgKKAmhiRodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5j -cmwwHQYDVR0OBBYEFMiBd4urSvDE0mt9B6CBjlPj8RMcMA4GA1UdDwEB/wQEAwIG -QDAKBggqhkjOPQQDAwNpADBmAjEA/dDkzO4iuKidm3IP4j+5JKOrzhn1+XO7WzbM -Uvu3+3Wn0zUAg88I0tAFPUHUsVqrAjEAzlmPXUdhTGEH7dGQowFHVnsSUP4o0Q/f -qqcQOidwglE0899fEZoQSLHJ6tR0K7ip ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGdDCCBfmgAwIBAgIQayQDbT+81MKPikMhkxHiEjAKBggqhkjOPQQDAzByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMB4XDTI1MDgzMTA1NTkyNVoXDTI4MDgzMDA1NTkyNFow +XzELMAkGA1UEBhMCRkkxFDASBgNVBAMMC01VU0VSLFVSTUFTMQ4wDAYDVQQEDAVN +VVNFUjEOMAwGA1UEKgwFVVJNQVMxGjAYBgNVBAUTEVBOT0ZJLTM5MDAzMDEyNzk4 +MIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAmX7GLKF5e8LxXrRNl5sM +AHA3UwM9/RKIjAePphFJ1IwEu4f1NutqGWs9hhsh6PnBnbnKsvN/US//5Bw/t37z +gBqgQaToPCww9zHsUFl0w3Q+XGxK3wzKltu50WaxIQaLExFTgFsPmViabT7M1Kqu +6UYlJQJQPkgkfvmuz3LeCFXFRpjEoIgvVfbAYxXfn8V1HPwPAhFTkC1iTht14SOE +WolghzV5R3IYeCey8Y978rFy24avN+ea1aweti98UaFH8wIjuX1zND7SHY2fu5tF ++uxSacJuBUukL0w34n3ODEPDJXojPEnJEIOZmJIV35jGcTMEc6OVlKHYBfwISUjy ++/dIy+oq/qz7z5Kr4gv2DBTLpEj6bCgS2uDDxVXZbDFY3K0jXtxVtk11pWKFUEtd +YJFV6FRswFHF/WYp8o2WJ7O0hlB47pkUgLtKXLNJrS8z+dLcP3s626ZSjfEPf3LD +Ku9GoDetBzvj+QanNUpzZGpWdaMNN4dpLdMSyp2WK6KK3TXowmYYSaXS2t+5MyM9 +0Om3JUkY9hIBIfeAgGwA9Vx8OT5srYU1zWIGcP8AeTsE2JDpCh11M56dAw8yF2z0 +JyTjYcmmX/Zla3gT76yhL43AHT64hxohpiZr8R2lKSFO1qg7ECly1XKov5Vm8rTd +CaDiWLrBwh4+XEJSludG6KZ+0Q8hrGU23qNBH/Yo4xh2vArwdaoKpHUiMbwu9/MZ +k7WTgCqxnp9fC6l+25ePfK4xiA5iVfZsmBs8iE8Z/J3akd2tZRW3jE672F1L+V6m +icbVCi8VFpWcVfdTZFvQPoozZAx8dm//L4hEEeu/7ylEmrn4luEcMsk386Pj61KB +wk54zzuxWc9i3u4JyW3B6F87LhGv01osVK35mXWhBI5hN8YrRHieHafuMGrfdGp9 +XrKRleYtih2BYr+FD2bfaxf0KEBaQdHMXEZ814x3SYLb1iv7/5yz6FNB37OwlIwa +VvoxaIggGXI0Ht0axQ6dqvDdo1cH+uXNYZJtp+ScQ6qVAgMBAAGjggG3MIIBszAJ +BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLNZ0LWqa/mBsLQHo63DzpXv8Y5GMHIGCCsG +AQUFBwEBBGYwZDA0BggrBgEFBQcwAoYoaHR0cDovL2Muc2suZWUvVEVTVF9FSUQt +TlFfMjAyMUUuZGVyLmNydDAsBggrBgEFBQcwAYYgaHR0cDovL2FpYS5kZW1vLnNr +LmVlL2VpZG5xMjAyMWUwMQYDVR0RBCowKKQmMCQxIjAgBgNVBAMMGVBOT0ZJLTM5 +MDAzMDEyNzk4LTlUMTctTlEweAYDVR0gBHEwbzBjBgkrBgEEAc4fEQEwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAgGBgQAj3oBATA1BgNV +HR8ELjAsMCqgKKAmhiRodHRwOi8vYy5zay5lZS90ZXN0X2VpZC1ucV8yMDIxZS5j +cmwwHQYDVR0OBBYEFMiBd4urSvDE0mt9B6CBjlPj8RMcMA4GA1UdDwEB/wQEAwIG +QDAKBggqhkjOPQQDAwNpADBmAjEA/dDkzO4iuKidm3IP4j+5JKOrzhn1+XO7WzbM +Uvu3+3Wn0zUAg88I0tAFPUHUsVqrAjEAzlmPXUdhTGEH7dGQowFHVnsSUP4o0Q/f +qqcQOidwglE0899fEZoQSLHJ6tR0K7ip +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/other-auth-cert.pem.crt b/src/test/resources/test-certs/other-auth-cert.pem.crt index e8f5b8a9..5ad5e65e 100644 --- a/src/test/resources/test-certs/other-auth-cert.pem.crt +++ b/src/test/resources/test-certs/other-auth-cert.pem.crt @@ -1,39 +1,39 @@ ------BEGIN CERTIFICATE----- -MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBo -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE -LVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcw -FQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ -Tk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTAL -BgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEw -DQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnU -hKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz -4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6z -lzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8 -l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpe -dy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0U -aE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0w -LTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2n -T5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339z -t7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKx -Kegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XK -ygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD -AgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRw -czovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1Ud -DgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XM -C2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4w -KQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsG -AQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNL -XzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifm -rjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2p -Kmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6v -ZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7 -grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4 -lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3Fa -YpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8D -j/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5o -PEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5 -m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql4 -40sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytf -q8s5bZci5vnHm110lnPhQxM= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGzTCCBLWgAwIBAgIQK3l/2aevBUlch9Q5lTgDfzANBgkqhkiG9w0BAQsFADBo +MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 +czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHDAaBgNVBAMME1RFU1Qgb2YgRUlE +LVNLIDIwMTYwIBcNMTkwMzEyMTU0NjAxWhgPMjAzMDEyMTcyMzU5NTlaMIGOMRcw +FQYDVQQLDA5BVVRIRU5USUNBVElPTjEoMCYGA1UEAwwfU01BUlQtSUQsREVNTyxQ +Tk9FRS0xMDEwMTAxMDAwNTEaMBgGA1UEBRMRUE5PRUUtMTAxMDEwMTAwMDUxDTAL +BgNVBCoMBERFTU8xETAPBgNVBAQMCFNNQVJULUlEMQswCQYDVQQGEwJFRTCCAiEw +DQYJKoZIhvcNAQEBBQADggIOADCCAgkCggIAWa3EyEHRT4SNHRQzW5V3FyMDuXnU +hKFKPjC9lWHscB1csyDsnN+wzLcSLmdhUb896fzAxIUTarNuQP8kuzF3MRqlgXJz +4yWVKLcFH/d3w9gs74tHmdRFf/xz3QQeM7cvktxinqqZP2ybW5VH3Kmni+Q25w6z +lzMY/Q0A72ES07TwfPY4v+n1n/2wpiDZhERbD1Y/0psCWc9zuZs0+R2BueZev0E8 +l1wOZi4HFRcee29GmIopAPCcbRqvZcfC62hAo2xvGCio5XC160B7B+AhMuu5jFpe +dy+lFKceqful5tUCUyorq+a5bj6YlQKC7rhCO/gY9t2bl3e4zgpdSsppXeHJGf0U +aE0FiC0MYW+cvayhqleeC8T1tGRrhnGsHcW/oXZ4WTfspvqUzhEwLircshvE0l0w +LTidehBuYMrmipjqZQ434hNyzvqci/7xq3H3fqU9Zf8llelHhNpj0DAsSRZ0D+2n +T5ril8aiS1LJeMraAaO4Q6vOjhn7XEKtCctxWIP1lmv2VwkTZREE8jVJgxKM339z +t7bALOItj5EuJ9NwUUyIEBi1iC5uB9B98kK4isvxOK325E8zunEze/4+bVgkUpKx +Kegk8DFkCRVcWF0mNfQ0odx05IJNMJoK8htZMZVIiIgECtFCbQHGpy56OJc6l3XK +ygDGh7tGwyEl/EcCAwEAAaOCAUkwggFFMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD +AgSwMFUGA1UdIAROMEwwQAYKKwYBBAHOHwMRAjAyMDAGCCsGAQUFBwIBFiRodHRw +czovL3d3dy5zay5lZS9lbi9yZXBvc2l0b3J5L0NQUy8wCAYGBACPegECMB0GA1Ud +DgQWBBTSw76xtK7AEN3t8SlpS2vc1GJJeTAfBgNVHSMEGDAWgBSusOrhNvgmq6XM +C2ZV/jodAr8StDATBgNVHSUEDDAKBggrBgEFBQcDAjB8BggrBgEFBQcBAQRwMG4w +KQYIKwYBBQUHMAGGHWh0dHA6Ly9haWEuZGVtby5zay5lZS9laWQyMDE2MEEGCCsG +AQUFBzAChjVodHRwOi8vc2suZWUvdXBsb2FkL2ZpbGVzL1RFU1Rfb2ZfRUlELVNL +XzIwMTYuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAtWc+LIkBzcsiqy2yYifm +rjprNu+PPsjyAexqpBJ61GUTN/NUMPYDTUaKoBEaxfrm+LcAzPmXmsiRUwCqHo2p +Kmonx57+diezL3GOnC5ZqXa8AkutNUrTYPvq1GM6foMmq0Ku73mZmQK6vAFcZQ6v +ZDIUgDPBlVP9mVZeYLPB2BzO49dVsx9X6nZIDH3corDsNS48MJ51CzV434NMP+T7 +grI3UtMGYqQ/rKOzFxMwn/x8GnnwO+YRH6Q9vh6k3JGrVlhxBA/6hgPUpxziiTR4 +lkdGCRVQXmVLopPhM/L0PaUfB6R3TG8iOBKgzGGIx8qyYMQ1e52/bQZ+taR1L3Fa +YpzaYi5tfQ6iMq66Nj/Sthj4illB99iphcSAlaoSfKAq7PLjucmxULiyXfRHQN8D +j/15Vh/jNthAHFJiFS9EDqB74IMGRX7BATRdtV5MY37fDDNrGqlkTylMdGK5jz5o +PEMVTwCWKHDZI+RwlWwHkKlEqzYW7bZ8Nh0aXiKoOWROa50Tl3HuQAqaht/buui5 +m5abVsDej7309j7LsCF1vmG4xkA0nV+qFiWshDcTKSjglUFqmfVciIGAoqgfuql4 +40sH4Jk+rhcPCQuKDOUZtRBjnj4vChjjRoGCOS8NH1VnpzEfgEBh6bv4Yaolxytf +q8s5bZci5vnHm110lnPhQxM= +-----END CERTIFICATE----- diff --git a/src/test/resources/test-certs/sign-cert-40504040001.pem.crt b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt index b49f0d45..e27cb966 100644 --- a/src/test/resources/test-certs/sign-cert-40504040001.pem.crt +++ b/src/test/resources/test-certs/sign-cert-40504040001.pem.crt @@ -1,42 +1,42 @@ ------BEGIN CERTIFICATE----- -MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww -KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB -UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj -MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK -VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw -MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ -q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw -SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL -r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma -tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB -0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W -rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY -1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp -RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH -48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 -nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk -aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e -wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk -pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM -ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f -jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY -xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC -izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG -CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F -SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u -c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 -MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr -BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv -Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD -VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF -BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA -jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov -L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 -c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov -L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU -1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 -7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC -MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld -CQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHTTCCBtSgAwIBAgIQZjAo7ibA2G30zeIncWmIlTAKBggqhkjOPQQDAzBxMSww +KgYDVQQDDCNURVNUIG9mIFNLIElEIFNvbHV0aW9ucyBFSUQtUSAyMDI0RTEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBB +UzELMAkGA1UEBhMCRUUwHhcNMjQxMDE1MTY0NDEyWhcNMjcxMDE1MTY0NDExWjBj +MQswCQYDVQQGEwJFRTEWMBQGA1UEAwwNVEVTVE5VTUJFUixPSzETMBEGA1UEBAwK +VEVTVE5VTUJFUjELMAkGA1UEKgwCT0sxGjAYBgNVBAUTEVBOT0VFLTQwNTA0MDQw +MDAxMIIDIjANBgkqhkiG9w0BAQEFAAOCAw8AMIIDCgKCAwEAjJyjWNg1OUr/mY4/ +q0Ba/oGnOuCQ5MUJIdzeyfc9LX0dRwZQFR6u426ULT0VNxgBqUabg7JaO63wjraw +SyYWwWB0kcbMcElYOnke5Z6LeFcq57/c248n20Lg/55DqpiHiIuentZt0W5Q6aCL +r6baVIwqIfsfEehOIwsAzhTd4MHOwGlsi4xaA7862yVQl2iH7MJAIl3XDxHf8sma +tmCXtf5/wsBl/Dd02RCV7simBjSp0i+lM4bF5BJB/np8JtRKIrMfo3o5Wv58b/dB +0dS1KpDA9qvY0jqVMtA7Pt+jnw6bO2aRFMeesJItnK+DUR3u2uuGJKPvn5s0Te+W +rR4E239bJ+U0VJd2qF3d5VTFh39un3GjwZ7GILEP/hc5AKaAsyXr5ReIUi0pqCHY +1qVL3CD0RR0NpmrKx8MA0b6D7OaovruiG59204q+Vg5I4N2kO2R0CTLPhapuu/kp +RKvax5DI2loh0l3oXRIDAoB5w9Yy99mittsfUWMiiDro18++Xf7qr5y71PlEKeDH +48k7iNQCVggrRMiSmNzOFruL0E8/utwTcxqTtA7weYrLUjjPutUA4RYDXhfdSkG4 +nneSRTTMrG+1e8d07ctxjjcmIe7LY33MdIe5XhyxXM4bmph69byYwSXXuXPj2QXk +aaLnm2NeV/LJ8/U7yXUpYJTrBKvpu60GCSexB9fHLClir1B/DrwZGcxPiJuFnF4e +wa9yVUhxT1WckqLZ+x492UyS7s8TiSZGoXU5nd/XXcNx2bkhlrzDyKkR79J0vNGk +pkqAO61Z2cbzTeEXJdhekNrZsIdOw93A8x5ZTCejbaE5hI+E4Vo7W+joAiURozTM +ljIiJXm1niE1q+U3/hmSNGGBgRRpbFXLxVYOvdLSZbFGN2BZKB3/Z5UqWOvc3L8f +jGnxnZSzO+rdJpVL30o6+VD9s7ZpIy4QtGBpnmaX3oLwL+E1vhaOkCVFzOyeWyVY +xH0INmrNDzOlTc6jHS6B0sRHjnZr/jHFEl9BLV3ItXQl91ODAgMBAAGjggKPMIIC +izAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFLAkFxmI42b4zShYZXtNFNiSZk9rMHAG +CCsGAQUFBwEBBGQwYjAzBggrBgEFBQcwAoYnaHR0cDovL2Muc2suZWUvVEVTVF9F +SUQtUV8yMDI0RS5kZXIuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vYWlhLmRlbW8u +c2suZWUvZWlkcTIwMjRlMDAGA1UdEQQpMCekJTAjMSEwHwYDVQQDDBhQTk9FRS00 +MDUwNDA0MDAwMS1NT0NLLVEweQYDVR0gBHIwcDBjBgkrBgEEAc4fEQIwVjBUBggr +BgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNvdXJjZXMv +Y2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMAkGBwQAi+xAAQIwKAYD +VR0JBCEwHzAdBggrBgEFBQcJATERGA8xOTA1MDQwNDEyMDAwMFowga4GCCsGAQUF +BwEDBIGhMIGeMBUGCCsGAQUFBwsCMAkGBwQAi+xJAQEwCAYGBACORgEBMAgGBgQA +jkYBBDATBgYEAI5GAQYwCQYHBACORgEGATBcBgYEAI5GAQUwUjBQFkpodHRwczov +L3d3dy5za2lkc29sdXRpb25zLmV1L3Jlc291cmNlcy9jb25kaXRpb25zLWZvci11 +c2Utb2YtY2VydGlmaWNhdGVzLxMCZW4wNAYDVR0fBC0wKzApoCegJYYjaHR0cDov +L2Muc2suZWUvdGVzdF9laWQtcV8yMDI0ZS5jcmwwHQYDVR0OBBYEFEByj2lyTYLU +1/8DXEqaJG4BH4SyMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjOPQQDAwNnADBkAjA5 +7Y0e2M/L3+f1b4WBuPCvBDImwDQdxoP7ziffv98OqfyEq3Zh5GKgh6lzWz3QN1sC +MCEsxVYv1ruojw4H3+IdMKfQJJxCJGMDUHPRyBj22wL++CWjm8PIh598MJqeozld +CQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt index 2572e4c8..ae2033e6 100644 --- a/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt +++ b/src/test/resources/trusted_certificates/TEST_EID-NQ_2021E.pem.crt @@ -1,22 +1,22 @@ ------BEGIN CERTIFICATE----- -MIIDpTCCAwagAwIBAgIQTiO7d7Wr6Flg+BPaeYgVHDAKBggqhkjOPQQDAzBuMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgUk9PVCBHMUUwHhcNMjEwNzIxMTIzMjI2WhcNMzYwNzIxMTIzMjI2WjByMQsw -CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh -DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv -bnMgRUlELU5RIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBn6bE+DVXUwO -8gYWoA6tu2gb4ou3Gk55ge6jYehcxehS5RO3GaknTrc2YrLcq6nwrcBoIrkVlDOd -Bfub4oea3zL7VlA/ADQ8PTYexu+0zxk1TEtsj0KHH9lh8f7FR1awo4IBYzCCAV8w -HwYDVR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFLNZ0LWq -a/mBsLQHo63DzpXv8Y5GMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/ -AgEAMGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2su -ZWUvb2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09U -X0cxXzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2su -ZWUvVEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAw -PTA7BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9y -ZXBvc2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYwAMIGIAkIBsJ6X9zwyHP3b28br -WIsid0vqWxOzPFU4GFTH/AqXW71V9WLNBJHsbuBg2VNi4k7CKUW7MpRqL8UI8QX7 -/X7jFxMCQgF+IPUDMXMsV99sgqo/Y6VkZYqiakayHkvECkJCncUfmpqVYUlcAxeZ -zRlYIOz3F5AvYJTrtMP0TR3yASD1GtYs4A== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDpTCCAwagAwIBAgIQTiO7d7Wr6Flg+BPaeYgVHDAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEpMCcGA1UEAwwgVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgUk9PVCBHMUUwHhcNMjEwNzIxMTIzMjI2WhcNMzYwNzIxMTIzMjI2WjByMQsw +CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh +DA5OVFJFRS0xMDc0NzAxMzEtMCsGA1UEAwwkVEVTVCBvZiBTSyBJRCBTb2x1dGlv +bnMgRUlELU5RIDIwMjFFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEBn6bE+DVXUwO +8gYWoA6tu2gb4ou3Gk55ge6jYehcxehS5RO3GaknTrc2YrLcq6nwrcBoIrkVlDOd +Bfub4oea3zL7VlA/ADQ8PTYexu+0zxk1TEtsj0KHH9lh8f7FR1awo4IBYzCCAV8w +HwYDVR0jBBgwFoAU4hzeY9y++IR+ATsuS4Cx4X/V8eYwHQYDVR0OBBYEFLNZ0LWq +a/mBsLQHo63DzpXv8Y5GMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/ +AgEAMGwGCCsGAQUFBwEBBGAwXjAiBggrBgEFBQcwAYYWaHR0cDovL2RlbW8uc2su +ZWUvb2NzcDA4BggrBgEFBQcwAoYsaHR0cDovL2Muc2suZWUvVEVTVF9TS19ST09U +X0cxXzIwMjFFLmRlci5jcnQwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL2Muc2su +ZWUvVEVTVF9TS19ST09UX0cxXzIwMjFFLmNybDBQBgNVHSAESTBHMEUGBFUdIAAw +PTA7BggrBgEFBQcCARYvaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9lbi9y +ZXBvc2l0b3J5L0NQUy8wCgYIKoZIzj0EAwMDgYwAMIGIAkIBsJ6X9zwyHP3b28br +WIsid0vqWxOzPFU4GFTH/AqXW71V9WLNBJHsbuBg2VNi4k7CKUW7MpRqL8UI8QX7 +/X7jFxMCQgF+IPUDMXMsV99sgqo/Y6VkZYqiakayHkvECkJCncUfmpqVYUlcAxeZ +zRlYIOz3F5AvYJTrtMP0TR3yASD1GtYs4A== +-----END CERTIFICATE----- diff --git a/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt index b6d7af73..5ca038c0 100644 --- a/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt +++ b/src/test/resources/trusted_certificates/TEST_of_SK_ID_Solutions_EID-Q_2024E.pem.crt @@ -1,23 +1,23 @@ ------BEGIN CERTIFICATE----- -MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw -bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG -A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s -dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow -cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx -FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv -bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 -oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh -vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC -AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L -gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr -LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo -dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw -VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv -dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy -MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j -cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB -BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f -4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo -XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa -7Wy8pf2lw6EcfyU= ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxzCCAymgAwIBAgIUIJ92Wg42THMIC1QSOpWpxv3+22AwCgYIKoZIzj0EAwMw +bjELMAkGA1UEBhMCRUUxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzEXMBUG +A1UEYQwOTlRSRUUtMTA3NDcwMTMxKTAnBgNVBAMMIFRFU1Qgb2YgU0sgSUQgU29s +dXRpb25zIFJPT1QgRzFFMB4XDTI0MDYwMzEzMDEyMloXDTM5MDUzMTEzMDEyMVow +cTEsMCoGA1UEAwwjVEVTVCBvZiBTSyBJRCBTb2x1dGlvbnMgRUlELVEgMjAyNEUx +FzAVBgNVBGEMDk5UUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlv +bnMgQVMxCzAJBgNVBAYTAkVFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE9tnu4Hr6 +oZ3virQ52FkQ8zgSnRLjSpbr7y6hjaI5ZtvFTssL3aOgvULxOvV5x+HtOmcGVfmh +vy9YtoJENq/E3pFFOkofrkX3O/RVLdtPpiVahYa89HCgqoEVDln5ILMWo4IBgzCC +AX8wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBTiHN5j3L74hH4BOy5L +gLHhf9Xx5jBsBggrBgEFBQcBAQRgMF4wOAYIKwYBBQUHMAKGLGh0dHA6Ly9jLnNr +LmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5kZXIuY3J0MCIGCCsGAQUFBzABhhZo +dHRwOi8vZGVtby5zay5lZS9vY3NwMHAGA1UdIARpMGcwBgYEVR0gADBdBgNVHSAw +VjBUBggrBgEFBQcCARZIaHR0cHM6Ly93d3cuc2tpZHNvbHV0aW9ucy5ldS9yZXNv +dXJjZXMvY2VydGlmaWNhdGlvbi1wcmFjdGljZS1zdGF0ZW1lbnQvMDkGA1UdHwQy +MDAwLqAsoCqGKGh0dHA6Ly9jLnNrLmVlL1RFU1RfU0tfUk9PVF9HMV8yMDIxRS5j +cmwwHQYDVR0OBBYEFLAkFxmI42b4zShYZXtNFNiSZk9rMA4GA1UdDwEB/wQEAwIB +BjAKBggqhkjOPQQDAwOBiwAwgYcCQXIdNKdyvEhtB+48QZEXi2dgXiAjYD7O0D4f +4Y2KPajqrRcwd9KEYr/yFjK0JWYHqRFN47tMdYhisy7aFySEWmKcAkIBUbTJeSbo +XAKBT9+j2zQduKv8Eqb/AIQybcVXyP23w+1ujNkcQZMkok41nGOH2YNRP7aGsCZa +7Wy8pf2lw6EcfyU= +-----END CERTIFICATE-----